From 70d6646af1fe771707dd2d6286ba17350f26b2a2 Mon Sep 17 00:00:00 2001 From: kimyerak Date: Tue, 14 Jan 2025 19:44:20 +0900 Subject: [PATCH 01/11] Feat: timetable, privacy --- .../controller/PrivacyController.java | 53 ++++++++ .../controller/TimetableController.java | 52 +++++++ .../Devkor_project/dto/PrivacyDto.java | 48 +++++++ .../Devkor_project/dto/TimetableDto.java | 28 ++++ .../dto/TimetableWithDetailsDto.java | 24 ++++ .../example/Devkor_project/entity/Course.java | 6 + .../Devkor_project/entity/Privacy.java | 77 +++++++++++ .../Devkor_project/entity/Timetable.java | 64 +++++++++ .../repository/PrivacyRepository.java | 11 ++ .../repository/TimetableRepository.java | 22 +++ .../service/PrivacyService.java | 123 +++++++++++++++++ .../service/TimetableService.java | 127 ++++++++++++++++++ 12 files changed, 635 insertions(+) create mode 100644 src/main/java/com/example/Devkor_project/controller/PrivacyController.java create mode 100644 src/main/java/com/example/Devkor_project/controller/TimetableController.java create mode 100644 src/main/java/com/example/Devkor_project/dto/PrivacyDto.java create mode 100644 src/main/java/com/example/Devkor_project/dto/TimetableDto.java create mode 100644 src/main/java/com/example/Devkor_project/dto/TimetableWithDetailsDto.java create mode 100644 src/main/java/com/example/Devkor_project/entity/Privacy.java create mode 100644 src/main/java/com/example/Devkor_project/entity/Timetable.java create mode 100644 src/main/java/com/example/Devkor_project/repository/PrivacyRepository.java create mode 100644 src/main/java/com/example/Devkor_project/repository/TimetableRepository.java create mode 100644 src/main/java/com/example/Devkor_project/service/PrivacyService.java create mode 100644 src/main/java/com/example/Devkor_project/service/TimetableService.java diff --git a/src/main/java/com/example/Devkor_project/controller/PrivacyController.java b/src/main/java/com/example/Devkor_project/controller/PrivacyController.java new file mode 100644 index 0000000..a5cd26e --- /dev/null +++ b/src/main/java/com/example/Devkor_project/controller/PrivacyController.java @@ -0,0 +1,53 @@ +package com.example.Devkor_project.controller; + +import com.example.Devkor_project.dto.PrivacyDto; +import com.example.Devkor_project.service.PrivacyService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import jakarta.validation.Valid; + +import java.util.List; + +@RestController +@RequestMapping("/api/privacy") +@RequiredArgsConstructor +@Tag(name = "Privacy 개인일정", description = "개인일정(privacy) 관련 api입니다.") +public class PrivacyController { + + private final PrivacyService privacyService; + + @GetMapping + public List getAllPrivacy(@RequestParam Long timetableId) { + return privacyService.getAllPrivacy(timetableId); + } + + @PostMapping + public PrivacyDto createPrivacy(@Valid @RequestBody PrivacyDto privacyDto) { + return privacyService.createPrivacy(privacyDto); + } + + @PutMapping("/{id}") + public PrivacyDto updatePrivacy(@PathVariable Long id, @Valid @RequestBody PrivacyDto privacyDto) { + return privacyService.updatePrivacy(id, privacyDto); + } + + @DeleteMapping("/{id}") + @Operation(summary = "개인일정 삭제") + public void deletePrivacy(@PathVariable Long id) { + privacyService.deletePrivacy(id); + } + + @PostMapping("/{timetableId}/privacy") + public PrivacyDto addPrivacyToTimetable(@PathVariable Long timetableId, @Valid @RequestBody PrivacyDto privacyDto) { + return privacyService.addPrivacyToTimetable(timetableId, privacyDto); + } + + @DeleteMapping("/{timetableId}/privacy/{privacyId}") + @Operation(summary = "해당 timetable에서 개인일정 삭제") + public void removePrivacyFromTimetable(@PathVariable Long timetableId, @PathVariable Long privacyId) { + privacyService.removePrivacyFromTimetable(timetableId, privacyId); + } + +} diff --git a/src/main/java/com/example/Devkor_project/controller/TimetableController.java b/src/main/java/com/example/Devkor_project/controller/TimetableController.java new file mode 100644 index 0000000..1010dba --- /dev/null +++ b/src/main/java/com/example/Devkor_project/controller/TimetableController.java @@ -0,0 +1,52 @@ +package com.example.Devkor_project.controller; + +import com.example.Devkor_project.dto.PrivacyDto; +import com.example.Devkor_project.entity.Timetable; +import com.example.Devkor_project.service.TimetableService; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import jakarta.validation.Valid; + +import java.util.List; + +@RestController +@RequestMapping("/api/timetables") +@RequiredArgsConstructor +@Tag(name = "Timetable 시간표", description = "시간표(timetable) 관련 api입니다.") +public class TimetableController { + + private final TimetableService timetableService; + + @PostMapping("/profile/{username}") + public Timetable createTimetable(@PathVariable String username) { + return timetableService.createTimetableForProfile(username); + } + + @PostMapping("/{id}/course") + public void addCourseToTimetable(@PathVariable Long id, @RequestParam Long courseId) { + timetableService.addCourseToTimetable(id, courseId); + } + + @DeleteMapping("/{id}/course/{courseId}") + public void removeCourseFromTimetable(@PathVariable Long id, @PathVariable Long courseId) { + timetableService.removeCourseFromTimetable(id, courseId); + } + + @PostMapping("/{id}/privacy") + public void addPrivacyToTimetable(@PathVariable Long id, @RequestBody PrivacyDto privacyDto) { + timetableService.addPrivacyToTimetable(id, privacyDto); + } + + @DeleteMapping("/{id}/privacy/{privacyId}") + public void removePrivacyFromTimetable(@PathVariable Long id, @PathVariable Long privacyId) { + timetableService.removePrivacyFromTimetable(id, privacyId); + } + + @GetMapping("/profile/{profileId}") + public List getTimetableByProfile(@PathVariable Long profileId) { + return timetableService.getTimetableByProfileId(profileId); + } + +} + diff --git a/src/main/java/com/example/Devkor_project/dto/PrivacyDto.java b/src/main/java/com/example/Devkor_project/dto/PrivacyDto.java new file mode 100644 index 0000000..19f81c6 --- /dev/null +++ b/src/main/java/com/example/Devkor_project/dto/PrivacyDto.java @@ -0,0 +1,48 @@ +package com.example.Devkor_project.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalTime; +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Builder +public class PrivacyDto { + + @Schema(description = "Privacy ID") + private Long id; + + @NotNull(message = "[timetableId] cannot be null.") + @Schema(description = "Timetable IDs") // Timetable과 연결된 ID + private List timetableIds; + + @NotBlank(message = "[name] cannot be blank.") + @Schema(description = "개인 일정 이름") + private String name; + + @NotBlank(message = "[day] cannot be blank.") + @Schema(description = "요일 (e.g., 월, 화, 수)") + private String day; + + @NotNull(message = "[startTime] cannot be null.") + @Schema(description = "시작 시간") + private LocalTime startTime; + + @NotNull(message = "[finishTime] cannot be null.") + @Schema(description = "종료 시간") + private LocalTime finishTime; + + @Schema(description = "장소") + private String location; + + @Schema(description = "생성 시간") + private LocalDateTime createdAt; + + @Schema(description = "수정 시간") + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/example/Devkor_project/dto/TimetableDto.java b/src/main/java/com/example/Devkor_project/dto/TimetableDto.java new file mode 100644 index 0000000..abd8ba9 --- /dev/null +++ b/src/main/java/com/example/Devkor_project/dto/TimetableDto.java @@ -0,0 +1,28 @@ +package com.example.Devkor_project.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@ToString +@Getter +@Builder +@Schema(description = "응답 성공 DTO") +public class TimetableDto { + + @Schema(description = "Timetable ID") + private Long id; + + @NotNull(message = "[profile_id] cannot be null.") + @Schema(description = "Profile ID") + private Long profileId; + + @NotBlank(message = "[name] cannot be blank.") + @Schema(description = "시간표 이름 (예: 25-1학기)") + private String name; +} diff --git a/src/main/java/com/example/Devkor_project/dto/TimetableWithDetailsDto.java b/src/main/java/com/example/Devkor_project/dto/TimetableWithDetailsDto.java new file mode 100644 index 0000000..571bcae --- /dev/null +++ b/src/main/java/com/example/Devkor_project/dto/TimetableWithDetailsDto.java @@ -0,0 +1,24 @@ +package com.example.Devkor_project.dto; + + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@ToString +@Getter +@Builder +@Schema(description = "응답 성공 DTO") +public class TimetableWithDetailsDto { + + private Long id; + private Long profileId; + private String name; + private List courses; // 연관된 Course 정보 + private List privacies; // 연관된 Privacy 정보 +} diff --git a/src/main/java/com/example/Devkor_project/entity/Course.java b/src/main/java/com/example/Devkor_project/entity/Course.java index 73f1e81..fb5e7b8 100644 --- a/src/main/java/com/example/Devkor_project/entity/Course.java +++ b/src/main/java/com/example/Devkor_project/entity/Course.java @@ -5,6 +5,8 @@ import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; @Entity @@ -36,4 +38,8 @@ public class Course @Column(nullable = false) private String semester; @Column private String credit; @Column(nullable = false) private int COUNT_comments; + + // Timetable와의 N:N 관계 추가 + @ManyToMany(mappedBy = "courses") // Timetable에서 설정한 필드명 + private List timetables = new ArrayList<>(); } diff --git a/src/main/java/com/example/Devkor_project/entity/Privacy.java b/src/main/java/com/example/Devkor_project/entity/Privacy.java new file mode 100644 index 0000000..2e5a965 --- /dev/null +++ b/src/main/java/com/example/Devkor_project/entity/Privacy.java @@ -0,0 +1,77 @@ +package com.example.Devkor_project.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Privacy { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; // 개인 일정 이름 + + @Column(nullable = false) + private String day; // 요일 (e.g., 월, 화, 수) + + @Column(nullable = false) + private LocalTime startTime; // 시작 시간 + + @Column(nullable = false) + private LocalTime finishTime; // 종료 시간 + + private String location; // 장소 + + // Timetable과 N:N 관계 설정 + @ManyToMany + @JoinTable( + name = "privacy_timetable", + joinColumns = @JoinColumn(name = "privacy_id"), + inverseJoinColumns = @JoinColumn(name = "timetable_id") + ) + private List timetables = new ArrayList<>(); + + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(nullable = false) + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } + // Builder에 timetables 처리 추가 + public static class PrivacyBuilder { + private List timetables = new ArrayList<>(); + + public PrivacyBuilder timetable(Timetable timetable) { + this.timetables.add(timetable); + return this; + } + + public PrivacyBuilder timetables(List timetables) { + this.timetables = timetables; + return this; + } + } + +} diff --git a/src/main/java/com/example/Devkor_project/entity/Timetable.java b/src/main/java/com/example/Devkor_project/entity/Timetable.java new file mode 100644 index 0000000..d54d214 --- /dev/null +++ b/src/main/java/com/example/Devkor_project/entity/Timetable.java @@ -0,0 +1,64 @@ +package com.example.Devkor_project.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Timetable +{ + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id", nullable = false) + private Profile profile; + + @Column(nullable = false) + private String name; // 시간표 이름 (예: 25-1학기) + + // N:N 관계 - Course + @ManyToMany + @JoinTable( + name = "course_timetable", // 연결 테이블 이름 + joinColumns = @JoinColumn(name = "timetable_id"), // Timetable의 FK + inverseJoinColumns = @JoinColumn(name = "course_id") // Course의 FK + ) + private List courses = new ArrayList<>(); + + // N:N 관계 - Privacy + @ManyToMany + @JoinTable( + name = "privacy_timetable", // 연결 테이블 이름 + joinColumns = @JoinColumn(name = "timetable_id"), // Timetable의 FK + inverseJoinColumns = @JoinColumn(name = "privacy_id") // Privacy의 FK + ) + private List privacies = new ArrayList<>(); + + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(nullable = false) + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/example/Devkor_project/repository/PrivacyRepository.java b/src/main/java/com/example/Devkor_project/repository/PrivacyRepository.java new file mode 100644 index 0000000..3c925e3 --- /dev/null +++ b/src/main/java/com/example/Devkor_project/repository/PrivacyRepository.java @@ -0,0 +1,11 @@ +package com.example.Devkor_project.repository; + +import com.example.Devkor_project.entity.Privacy; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface PrivacyRepository extends JpaRepository { + + // timetables 컬렉션에 포함된 timetable의 ID를 기준으로 쿼리 수정 + List findByTimetables_Id(Long timetableId);} diff --git a/src/main/java/com/example/Devkor_project/repository/TimetableRepository.java b/src/main/java/com/example/Devkor_project/repository/TimetableRepository.java new file mode 100644 index 0000000..d4bdd3a --- /dev/null +++ b/src/main/java/com/example/Devkor_project/repository/TimetableRepository.java @@ -0,0 +1,22 @@ +package com.example.Devkor_project.repository; + +import com.example.Devkor_project.entity.Timetable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface TimetableRepository extends JpaRepository { + + // 특정 Profile의 모든 시간표 검색 + @Query("SELECT t FROM Timetable t WHERE t.profile.profile_id = :profileId") + List findByProfile_profileId(@Param("profileId") Long profileId); + + // 그 프로필 가진 사람의 특정 시간표(Timetable ID) 검색 + @Query("SELECT t FROM Timetable t WHERE t.id = :timetableId AND t.profile.profile_id = :profileId") + Timetable findByIdAndProfile_profileId(@Param("timetableId") Long timetableId, @Param("profileId") Long profileId); + +} diff --git a/src/main/java/com/example/Devkor_project/service/PrivacyService.java b/src/main/java/com/example/Devkor_project/service/PrivacyService.java new file mode 100644 index 0000000..d6f65a2 --- /dev/null +++ b/src/main/java/com/example/Devkor_project/service/PrivacyService.java @@ -0,0 +1,123 @@ +package com.example.Devkor_project.service; + +import com.example.Devkor_project.dto.PrivacyDto; +import com.example.Devkor_project.entity.Privacy; +import com.example.Devkor_project.entity.Timetable; +import com.example.Devkor_project.repository.PrivacyRepository; +import com.example.Devkor_project.repository.TimetableRepository; +import lombok.RequiredArgsConstructor; +import jakarta.transaction.Transactional; +import org.springframework.stereotype.Service; + + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PrivacyService { + + private final PrivacyRepository privacyRepository; + private final TimetableRepository timetableRepository; + + @Transactional + public List getAllPrivacy(Long timetableId) { + return privacyRepository.findByTimetables_Id(timetableId).stream() + .map(this::convertToDto) + .collect(Collectors.toList()); + } + + @Transactional + public PrivacyDto createPrivacy(PrivacyDto privacyDto) { + // 해당하는 모든 Timetable 가져오기 + List timetables = timetableRepository.findAllById(privacyDto.getTimetableIds()); + + Privacy privacy = Privacy.builder() + .timetables(timetables) + .name(privacyDto.getName()) + .day(privacyDto.getDay()) + .startTime(privacyDto.getStartTime()) + .finishTime(privacyDto.getFinishTime()) + .location(privacyDto.getLocation()) + .build(); + + Privacy savedPrivacy = privacyRepository.save(privacy); + return convertToDto(savedPrivacy); + } + + @Transactional + public PrivacyDto updatePrivacy(Long id, PrivacyDto privacyDto) { + Privacy privacy = privacyRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Privacy not found")); + + List timetables = timetableRepository.findAllById(privacyDto.getTimetableIds()); + privacy.setName(privacyDto.getName()); + privacy.setDay(privacyDto.getDay()); + privacy.setStartTime(privacyDto.getStartTime()); + privacy.setFinishTime(privacyDto.getFinishTime()); + privacy.setLocation(privacyDto.getLocation()); + + Privacy updatedPrivacy = privacyRepository.save(privacy); + return convertToDto(updatedPrivacy); + } + + @Transactional + public void deletePrivacy(Long id) { + privacyRepository.deleteById(id); + } + + @Transactional + public PrivacyDto addPrivacyToTimetable(Long timetableId, PrivacyDto privacyDto) { + // timetableId로 Timetable을 조회 + Timetable timetable = timetableRepository.findById(timetableId) + .orElseThrow(() -> new RuntimeException("Timetable not found")); + + // Privacy 객체 생성 + Privacy privacy = Privacy.builder() + .name(privacyDto.getName()) + .day(privacyDto.getDay()) + .startTime(privacyDto.getStartTime()) + .finishTime(privacyDto.getFinishTime()) + .location(privacyDto.getLocation()) + .build(); + + // Privacy 객체에 Timetable 추가 + privacy.getTimetables().add(timetable); + + // Privacy 저장 + Privacy savedPrivacy = privacyRepository.save(privacy); + + // PrivacyDto로 변환하여 반환 + return convertToDto(savedPrivacy); + } + + @Transactional + public void removePrivacyFromTimetable(Long timetableId, Long privacyId) { + // Privacy 조회 + Privacy privacy = privacyRepository.findById(privacyId) + .orElseThrow(() -> new RuntimeException("Privacy not found")); + + // 특정 Timetable과 Privacy의 관계를 끊기 + privacy.getTimetables().removeIf(timetable -> timetable.getId().equals(timetableId)); + + // 변경 사항 저장 + privacyRepository.save(privacy); + } + + + private PrivacyDto convertToDto(Privacy privacy) { + return PrivacyDto.builder() + .id(privacy.getId()) + .timetableIds(privacy.getTimetables().stream() + .map(Timetable::getId) + .collect(Collectors.toList())) + .name(privacy.getName()) + .day(privacy.getDay()) + .startTime(privacy.getStartTime()) + .finishTime(privacy.getFinishTime()) + .location(privacy.getLocation()) + .createdAt(privacy.getCreatedAt()) + .updatedAt(privacy.getUpdatedAt()) + .build(); + } +} diff --git a/src/main/java/com/example/Devkor_project/service/TimetableService.java b/src/main/java/com/example/Devkor_project/service/TimetableService.java new file mode 100644 index 0000000..84534a5 --- /dev/null +++ b/src/main/java/com/example/Devkor_project/service/TimetableService.java @@ -0,0 +1,127 @@ +package com.example.Devkor_project.service; + +import com.example.Devkor_project.dto.PrivacyDto; +import com.example.Devkor_project.entity.Course; +import com.example.Devkor_project.entity.Privacy; +import com.example.Devkor_project.entity.Profile; +import com.example.Devkor_project.entity.Timetable; +import com.example.Devkor_project.repository.CourseRepository; +import com.example.Devkor_project.repository.PrivacyRepository; +import com.example.Devkor_project.repository.ProfileRepository; +import com.example.Devkor_project.repository.TimetableRepository; +import lombok.RequiredArgsConstructor; +import jakarta.transaction.Transactional; +import org.springframework.stereotype.Service; + +import java.util.List; + + +@Service +@RequiredArgsConstructor +public class TimetableService { + + private final TimetableRepository timetableRepository; + private final CourseRepository courseRepository; + private final PrivacyRepository privacyRepository; + private final ProfileRepository profileRepository; + + @Transactional + public Timetable createTimetableForProfile(String username) { + // username으로 Profile을 찾음 + Profile profile = profileRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("Profile not found")); + + // 새로운 Timetable 생성 + Timetable timetable = Timetable.builder() + .profile(profile) // 프로필과 연결된 시간표 생성 + .build(); + + // 시간표 저장 + return timetableRepository.save(timetable); + } + + @Transactional + public void addCourseToTimetable(Long timetableId, Long courseId) { + Timetable timetable = timetableRepository.findById(timetableId) + .orElseThrow(() -> new RuntimeException("Timetable not found")); + + Course course = courseRepository.findById(courseId) + .orElseThrow(() -> new RuntimeException("Course not found")); + + // Course와 Timetable 간의 연관 관계 추가 + if (!course.getTimetables().contains(timetable)) { + course.getTimetables().add(timetable); + } + if (!timetable.getCourses().contains(course)) { + timetable.getCourses().add(course); + } + + // 변경 사항 저장 + courseRepository.save(course); + timetableRepository.save(timetable); + } + + @Transactional + public void removeCourseFromTimetable(Long timetableId, Long courseId) { + Timetable timetable = timetableRepository.findById(timetableId) + .orElseThrow(() -> new RuntimeException("Timetable not found")); + + Course course = courseRepository.findById(courseId) + .orElseThrow(() -> new RuntimeException("Course not found")); + + // Course와 Timetable 간의 연관 관계 제거 + course.getTimetables().remove(timetable); + timetable.getCourses().remove(course); + + // 변경 사항 저장 + courseRepository.save(course); + timetableRepository.save(timetable); + } + + @Transactional + public void addPrivacyToTimetable(Long timetableId, PrivacyDto privacyDto) { + Timetable timetable = timetableRepository.findById(timetableId) + .orElseThrow(() -> new RuntimeException("Timetable not found")); + + Privacy privacy = Privacy.builder() + .timetable(timetable) + .name(privacyDto.getName()) + .day(privacyDto.getDay()) + .startTime(privacyDto.getStartTime()) + .finishTime(privacyDto.getFinishTime()) + .location(privacyDto.getLocation()) + .build(); + + privacyRepository.save(privacy); + } + + @Transactional + public void removePrivacyFromTimetable(Long timetableId, Long privacyId) { + Privacy privacy = privacyRepository.findById(privacyId) + .orElseThrow(() -> new RuntimeException("Privacy not found")); + + // timetableId가 privacy의 timetables 리스트에 포함되어 있는지 검사 + boolean isLinkedToTimetable = privacy.getTimetables().stream() + .anyMatch(timetable -> timetable.getId().equals(timetableId)); + + if (!isLinkedToTimetable) { + throw new RuntimeException("Privacy does not belong to this Timetable"); + } + + // Timetable과 Privacy 간의 연관 관계 해제 + privacy.getTimetables().removeIf(timetable -> timetable.getId().equals(timetableId)); + privacyRepository.save(privacy); + } + + // profileId로 timetable 조회 + public List getTimetableByProfileId(Long profileId) { + List timetables = timetableRepository.findByProfile_profileId(profileId); + + // timetable이 없다면 적절한 메시지를 반환 + if (timetables.isEmpty()) { + throw new RuntimeException("해당 프로필에 대한 timetable이 없습니다. 새로 만들어보세요!"); + } + + return timetables; + } +} From 6e7ad4fc5af36d22e456a2dd4318c2a9a24dcde0 Mon Sep 17 00:00:00 2001 From: kimyerak Date: Tue, 28 Jan 2025 22:44:51 +0900 Subject: [PATCH 02/11] =?UTF-8?q?fix:=20timetable,=20privacy=EC=9D=98=20he?= =?UTF-8?q?ader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PrivacyController.java | 76 +++++++-- .../controller/TimetableController.java | 68 +++++--- .../Devkor_project/dto/PrivacyDto.java | 3 +- .../Devkor_project/dto/TimetableDto.java | 4 - .../repository/PrivacyRepository.java | 11 +- .../repository/TimetableRepository.java | 7 + .../service/PrivacyService.java | 97 +++++++---- .../service/TimetableService.java | 150 ++++++++++-------- 8 files changed, 275 insertions(+), 141 deletions(-) diff --git a/src/main/java/com/example/Devkor_project/controller/PrivacyController.java b/src/main/java/com/example/Devkor_project/controller/PrivacyController.java index a5cd26e..4b05345 100644 --- a/src/main/java/com/example/Devkor_project/controller/PrivacyController.java +++ b/src/main/java/com/example/Devkor_project/controller/PrivacyController.java @@ -1,53 +1,95 @@ package com.example.Devkor_project.controller; import com.example.Devkor_project.dto.PrivacyDto; +import com.example.Devkor_project.dto.ResponseDto; import com.example.Devkor_project.service.PrivacyService; +import com.example.Devkor_project.configuration.VersionProvider; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; +import java.security.Principal; import java.util.List; @RestController @RequestMapping("/api/privacy") @RequiredArgsConstructor -@Tag(name = "Privacy 개인일정", description = "개인일정(privacy) 관련 api입니다.") +@Tag(name = "Privacy 개인일정", description = "개인일정(privacy) 관련 API입니다.") public class PrivacyController { private final PrivacyService privacyService; + private final VersionProvider versionProvider; @GetMapping - public List getAllPrivacy(@RequestParam Long timetableId) { - return privacyService.getAllPrivacy(timetableId); + @Operation(summary = "해당 사용자의 모든 개인일정 조회") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + public List getAllPrivacy(Principal principal) { + return privacyService.getAllPrivacy(principal); } + @GetMapping("/timetable/{timetableId}") + @Operation(summary = "특정 시간표의 개인일정 조회") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + @Parameter(in = ParameterIn.PATH, name = "timetableId", description = "조회할 시간표 ID") + public List getPrivacyByTimetable(@PathVariable Long timetableId, Principal principal) { + return privacyService.getPrivacyByTimetable(timetableId, principal); + } + + +// @PostMapping +// @Operation(summary = "개인일정 생성") +// @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") +// public PrivacyDto createPrivacy(@Valid @RequestBody PrivacyDto privacyDto, Principal principal) { +// return privacyService.createPrivacy(privacyDto, principal); +// } + @PostMapping - public PrivacyDto createPrivacy(@Valid @RequestBody PrivacyDto privacyDto) { - return privacyService.createPrivacy(privacyDto); + @Operation(summary = "개인일정 생성") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + public ResponseEntity createPrivacy(@Valid @RequestBody PrivacyDto privacyDto, Principal principal) { + PrivacyDto createdPrivacy = privacyService.createPrivacy(privacyDto, principal); + + return ResponseEntity.status(HttpStatus.OK) + .body(ResponseDto.Success.builder() + .message("개인일정이 성공적으로 생성되었습니다.") + .data(createdPrivacy) + .version(versionProvider.getVersion()) + .build() + ); } - @PutMapping("/{id}") - public PrivacyDto updatePrivacy(@PathVariable Long id, @Valid @RequestBody PrivacyDto privacyDto) { - return privacyService.updatePrivacy(id, privacyDto); + @PutMapping("/{privacyId}") + @Operation(summary = "개인일정 수정") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + @Parameter(in = ParameterIn.PATH, name = "privacyId", description = "수정할 개인일정 ID") + public PrivacyDto updatePrivacy(@PathVariable Long privacyId, @Valid @RequestBody PrivacyDto privacyDto, Principal principal) { + return privacyService.updatePrivacy(privacyId, privacyDto, principal); } - @DeleteMapping("/{id}") + @DeleteMapping("/{privacyId}") @Operation(summary = "개인일정 삭제") - public void deletePrivacy(@PathVariable Long id) { - privacyService.deletePrivacy(id); + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + public void deletePrivacy(@PathVariable Long privacyId, Principal principal) { + privacyService.deletePrivacy(privacyId, principal); } @PostMapping("/{timetableId}/privacy") - public PrivacyDto addPrivacyToTimetable(@PathVariable Long timetableId, @Valid @RequestBody PrivacyDto privacyDto) { - return privacyService.addPrivacyToTimetable(timetableId, privacyDto); + @Operation(summary = "특정 시간표에 개인일정 추가") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + public PrivacyDto addPrivacyToTimetable(@PathVariable Long timetableId, @Valid @RequestBody PrivacyDto privacyDto, Principal principal) { + return privacyService.addPrivacyToTimetable(timetableId, privacyDto, principal); } @DeleteMapping("/{timetableId}/privacy/{privacyId}") - @Operation(summary = "해당 timetable에서 개인일정 삭제") - public void removePrivacyFromTimetable(@PathVariable Long timetableId, @PathVariable Long privacyId) { - privacyService.removePrivacyFromTimetable(timetableId, privacyId); + @Operation(summary = "특정 시간표에서 개인일정 삭제") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + public void removePrivacyFromTimetable(@PathVariable Long timetableId, @PathVariable Long privacyId, Principal principal) { + privacyService.removePrivacyFromTimetable(timetableId, privacyId, principal); } - } diff --git a/src/main/java/com/example/Devkor_project/controller/TimetableController.java b/src/main/java/com/example/Devkor_project/controller/TimetableController.java index 1010dba..ae1a134 100644 --- a/src/main/java/com/example/Devkor_project/controller/TimetableController.java +++ b/src/main/java/com/example/Devkor_project/controller/TimetableController.java @@ -1,52 +1,72 @@ package com.example.Devkor_project.controller; import com.example.Devkor_project.dto.PrivacyDto; +import com.example.Devkor_project.dto.TimetableDto; +import com.example.Devkor_project.dto.ResponseDto; import com.example.Devkor_project.entity.Timetable; import com.example.Devkor_project.service.TimetableService; +import com.example.Devkor_project.configuration.VersionProvider; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import jakarta.validation.Valid; -import java.util.List; +import jakarta.validation.Valid; +import java.security.Principal; @RestController @RequestMapping("/api/timetables") @RequiredArgsConstructor -@Tag(name = "Timetable 시간표", description = "시간표(timetable) 관련 api입니다.") +@Tag(name = "Timetable 시간표", description = "시간표(timetable) 관련 API입니다.") public class TimetableController { private final TimetableService timetableService; + private final VersionProvider versionProvider; + + /** 🟢 로그인된 사용자 시간표 생성 */ + @PostMapping + @Operation(summary = "시간표 생성") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + public ResponseEntity createTimetable( + @Valid @RequestBody TimetableDto timetableDto, + Principal principal + ) { + Timetable timetable = timetableService.createTimetableForProfile(timetableDto, principal); - @PostMapping("/profile/{username}") - public Timetable createTimetable(@PathVariable String username) { - return timetableService.createTimetableForProfile(username); + return ResponseEntity.status(HttpStatus.OK) + .body(ResponseDto.Success.builder() + .message("시간표가 성공적으로 생성되었습니다.") + .data(timetable) + .version(versionProvider.getVersion()) + .build()); } + + /** 🟢 강의 추가 */ @PostMapping("/{id}/course") - public void addCourseToTimetable(@PathVariable Long id, @RequestParam Long courseId) { - timetableService.addCourseToTimetable(id, courseId); + @Operation(summary = "시간표에 강의 추가") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + public ResponseEntity addCourseToTimetable(@PathVariable Long id, @RequestParam Long courseId, Principal principal) { + return timetableService.addCourseToTimetable(id, courseId, principal); } + /** 🟢 강의 제거 */ @DeleteMapping("/{id}/course/{courseId}") - public void removeCourseFromTimetable(@PathVariable Long id, @PathVariable Long courseId) { - timetableService.removeCourseFromTimetable(id, courseId); + @Operation(summary = "시간표에서 강의 제거") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + public ResponseEntity removeCourseFromTimetable(@PathVariable Long id, @PathVariable Long courseId, Principal principal) { + return timetableService.removeCourseFromTimetable(id, courseId, principal); } + /** 🟢 개인 일정 추가 */ @PostMapping("/{id}/privacy") - public void addPrivacyToTimetable(@PathVariable Long id, @RequestBody PrivacyDto privacyDto) { - timetableService.addPrivacyToTimetable(id, privacyDto); - } - - @DeleteMapping("/{id}/privacy/{privacyId}") - public void removePrivacyFromTimetable(@PathVariable Long id, @PathVariable Long privacyId) { - timetableService.removePrivacyFromTimetable(id, privacyId); - } - - @GetMapping("/profile/{profileId}") - public List getTimetableByProfile(@PathVariable Long profileId) { - return timetableService.getTimetableByProfileId(profileId); + @Operation(summary = "시간표에 개인 일정 추가") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + public ResponseEntity addPrivacyToTimetable(@PathVariable Long id, @Valid @RequestBody PrivacyDto privacyDto, Principal principal) { + return timetableService.addPrivacyToTimetable(id, privacyDto, principal); } - } - diff --git a/src/main/java/com/example/Devkor_project/dto/PrivacyDto.java b/src/main/java/com/example/Devkor_project/dto/PrivacyDto.java index 19f81c6..c96d017 100644 --- a/src/main/java/com/example/Devkor_project/dto/PrivacyDto.java +++ b/src/main/java/com/example/Devkor_project/dto/PrivacyDto.java @@ -17,7 +17,8 @@ public class PrivacyDto { @Schema(description = "Privacy ID") private Long id; - @NotNull(message = "[timetableId] cannot be null.") +// @NotNull(message = "[timetableId] cannot be null... 널이어도 되나? 되는듯") + @Schema(description = "Timetable IDs") // Timetable과 연결된 ID private List timetableIds; diff --git a/src/main/java/com/example/Devkor_project/dto/TimetableDto.java b/src/main/java/com/example/Devkor_project/dto/TimetableDto.java index abd8ba9..3d6d2d8 100644 --- a/src/main/java/com/example/Devkor_project/dto/TimetableDto.java +++ b/src/main/java/com/example/Devkor_project/dto/TimetableDto.java @@ -18,10 +18,6 @@ public class TimetableDto { @Schema(description = "Timetable ID") private Long id; - @NotNull(message = "[profile_id] cannot be null.") - @Schema(description = "Profile ID") - private Long profileId; - @NotBlank(message = "[name] cannot be blank.") @Schema(description = "시간표 이름 (예: 25-1학기)") private String name; diff --git a/src/main/java/com/example/Devkor_project/repository/PrivacyRepository.java b/src/main/java/com/example/Devkor_project/repository/PrivacyRepository.java index 3c925e3..68de30a 100644 --- a/src/main/java/com/example/Devkor_project/repository/PrivacyRepository.java +++ b/src/main/java/com/example/Devkor_project/repository/PrivacyRepository.java @@ -8,4 +8,13 @@ public interface PrivacyRepository extends JpaRepository { // timetables 컬렉션에 포함된 timetable의 ID를 기준으로 쿼리 수정 - List findByTimetables_Id(Long timetableId);} + List findByTimetables_Id(Long timetableId); + // timetableIds 리스트와 연결된 모든 개인일정을 조회 + List findByTimetables_IdIn(List timetableIds); + + + // 특정 이름을 가진 개인일정을 조회 (예시로 추가) + List findByName(String name); + +} + diff --git a/src/main/java/com/example/Devkor_project/repository/TimetableRepository.java b/src/main/java/com/example/Devkor_project/repository/TimetableRepository.java index d4bdd3a..bfbf606 100644 --- a/src/main/java/com/example/Devkor_project/repository/TimetableRepository.java +++ b/src/main/java/com/example/Devkor_project/repository/TimetableRepository.java @@ -19,4 +19,11 @@ public interface TimetableRepository extends JpaRepository { @Query("SELECT t FROM Timetable t WHERE t.id = :timetableId AND t.profile.profile_id = :profileId") Timetable findByIdAndProfile_profileId(@Param("timetableId") Long timetableId, @Param("profileId") Long profileId); + // Profile 이메일을 기반으로 모든 시간표 조회 + List findAllByProfile_Email(String email); + + @Query("SELECT t FROM Timetable t WHERE t.profile.email = :email") + List findTimetablesByEmail(@Param("email") String email); + + } diff --git a/src/main/java/com/example/Devkor_project/service/PrivacyService.java b/src/main/java/com/example/Devkor_project/service/PrivacyService.java index d6f65a2..55f3c23 100644 --- a/src/main/java/com/example/Devkor_project/service/PrivacyService.java +++ b/src/main/java/com/example/Devkor_project/service/PrivacyService.java @@ -9,7 +9,7 @@ import jakarta.transaction.Transactional; import org.springframework.stereotype.Service; - +import java.security.Principal; import java.util.List; import java.util.stream.Collectors; @@ -20,17 +20,34 @@ public class PrivacyService { private final PrivacyRepository privacyRepository; private final TimetableRepository timetableRepository; + // 특정 시간표의 개인일정 조회 @Transactional - public List getAllPrivacy(Long timetableId) { + public List getPrivacyByTimetable(Long timetableId, Principal principal) { + validateTimetableOwnership(timetableId, principal); return privacyRepository.findByTimetables_Id(timetableId).stream() .map(this::convertToDto) .collect(Collectors.toList()); } + // 사용자의 모든 개인일정 조회 @Transactional - public PrivacyDto createPrivacy(PrivacyDto privacyDto) { - // 해당하는 모든 Timetable 가져오기 - List timetables = timetableRepository.findAllById(privacyDto.getTimetableIds()); + public List getAllPrivacy(Principal principal) { + String userEmail = principal.getName(); + + List timetableIds = timetableRepository.findAllByProfile_Email(userEmail) + .stream() + .map(Timetable::getId) + .collect(Collectors.toList()); + + return privacyRepository.findByTimetables_IdIn(timetableIds).stream() + .map(this::convertToDto) + .collect(Collectors.toList()); + } + + // 개인일정 생성 + @Transactional + public PrivacyDto createPrivacy(PrivacyDto privacyDto, Principal principal) { + List timetables = validateTimetablesOwnership(privacyDto.getTimetableIds(), principal); Privacy privacy = Privacy.builder() .timetables(timetables) @@ -41,38 +58,48 @@ public PrivacyDto createPrivacy(PrivacyDto privacyDto) { .location(privacyDto.getLocation()) .build(); - Privacy savedPrivacy = privacyRepository.save(privacy); - return convertToDto(savedPrivacy); + return convertToDto(privacyRepository.save(privacy)); } + // 개인일정 수정 @Transactional - public PrivacyDto updatePrivacy(Long id, PrivacyDto privacyDto) { + public PrivacyDto updatePrivacy(Long id, PrivacyDto privacyDto, Principal principal) { Privacy privacy = privacyRepository.findById(id) .orElseThrow(() -> new RuntimeException("Privacy not found")); + validateTimetablesOwnership(privacy.getTimetables().stream() + .map(Timetable::getId) + .collect(Collectors.toList()), principal); + List timetables = timetableRepository.findAllById(privacyDto.getTimetableIds()); privacy.setName(privacyDto.getName()); privacy.setDay(privacyDto.getDay()); privacy.setStartTime(privacyDto.getStartTime()); privacy.setFinishTime(privacyDto.getFinishTime()); privacy.setLocation(privacyDto.getLocation()); + privacy.setTimetables(timetables); - Privacy updatedPrivacy = privacyRepository.save(privacy); - return convertToDto(updatedPrivacy); + return convertToDto(privacyRepository.save(privacy)); } + // 개인일정 삭제 @Transactional - public void deletePrivacy(Long id) { + public void deletePrivacy(Long id, Principal principal) { + Privacy privacy = privacyRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Privacy not found")); + + validateTimetablesOwnership(privacy.getTimetables().stream() + .map(Timetable::getId) + .collect(Collectors.toList()), principal); + privacyRepository.deleteById(id); } + // 특정 시간표에 개인일정 추가 @Transactional - public PrivacyDto addPrivacyToTimetable(Long timetableId, PrivacyDto privacyDto) { - // timetableId로 Timetable을 조회 - Timetable timetable = timetableRepository.findById(timetableId) - .orElseThrow(() -> new RuntimeException("Timetable not found")); + public PrivacyDto addPrivacyToTimetable(Long timetableId, PrivacyDto privacyDto, Principal principal) { + Timetable timetable = validateTimetableOwnership(timetableId, principal); - // Privacy 객체 생성 Privacy privacy = Privacy.builder() .name(privacyDto.getName()) .day(privacyDto.getDay()) @@ -81,36 +108,48 @@ public PrivacyDto addPrivacyToTimetable(Long timetableId, PrivacyDto privacyDto) .location(privacyDto.getLocation()) .build(); - // Privacy 객체에 Timetable 추가 privacy.getTimetables().add(timetable); - - // Privacy 저장 - Privacy savedPrivacy = privacyRepository.save(privacy); - - // PrivacyDto로 변환하여 반환 - return convertToDto(savedPrivacy); + return convertToDto(privacyRepository.save(privacy)); } + // 특정 시간표에서 개인일정 삭제 @Transactional - public void removePrivacyFromTimetable(Long timetableId, Long privacyId) { - // Privacy 조회 + public void removePrivacyFromTimetable(Long timetableId, Long privacyId, Principal principal) { + validateTimetableOwnership(timetableId, principal); + Privacy privacy = privacyRepository.findById(privacyId) .orElseThrow(() -> new RuntimeException("Privacy not found")); - // 특정 Timetable과 Privacy의 관계를 끊기 privacy.getTimetables().removeIf(timetable -> timetable.getId().equals(timetableId)); - - // 변경 사항 저장 privacyRepository.save(privacy); } + // 시간표 소유권 확인 + private Timetable validateTimetableOwnership(Long timetableId, Principal principal) { + return timetableRepository.findById(timetableId) + .filter(timetable -> timetable.getProfile().getEmail().equals(principal.getName())) + .orElseThrow(() -> new RuntimeException("You do not have access to this timetable")); + } + + // 여러 시간표 소유권 확인 + private List validateTimetablesOwnership(List timetableIds, Principal principal) { + List timetables = timetableRepository.findAllById(timetableIds); + boolean isOwner = timetables.stream() + .allMatch(timetable -> timetable.getProfile().getEmail().equals(principal.getName())); + + if (!isOwner) { + throw new RuntimeException("You do not have access to one or more timetables"); + } + + return timetables; + } private PrivacyDto convertToDto(Privacy privacy) { return PrivacyDto.builder() .id(privacy.getId()) .timetableIds(privacy.getTimetables().stream() .map(Timetable::getId) - .collect(Collectors.toList())) + .collect(Collectors.toList())) .name(privacy.getName()) .day(privacy.getDay()) .startTime(privacy.getStartTime()) diff --git a/src/main/java/com/example/Devkor_project/service/TimetableService.java b/src/main/java/com/example/Devkor_project/service/TimetableService.java index 84534a5..014e97a 100644 --- a/src/main/java/com/example/Devkor_project/service/TimetableService.java +++ b/src/main/java/com/example/Devkor_project/service/TimetableService.java @@ -1,20 +1,27 @@ package com.example.Devkor_project.service; import com.example.Devkor_project.dto.PrivacyDto; +import com.example.Devkor_project.dto.ResponseDto; +import com.example.Devkor_project.dto.TimetableDto; import com.example.Devkor_project.entity.Course; import com.example.Devkor_project.entity.Privacy; import com.example.Devkor_project.entity.Profile; import com.example.Devkor_project.entity.Timetable; +import com.example.Devkor_project.exception.AppException; +import com.example.Devkor_project.exception.ErrorCode; import com.example.Devkor_project.repository.CourseRepository; import com.example.Devkor_project.repository.PrivacyRepository; import com.example.Devkor_project.repository.ProfileRepository; import com.example.Devkor_project.repository.TimetableRepository; -import lombok.RequiredArgsConstructor; import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import java.security.Principal; import java.util.List; - +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -25,66 +32,88 @@ public class TimetableService { private final PrivacyRepository privacyRepository; private final ProfileRepository profileRepository; + /** 🟢 로그인된 사용자의 시간표 생성 */ @Transactional - public Timetable createTimetableForProfile(String username) { - // username으로 Profile을 찾음 - Profile profile = profileRepository.findByUsername(username) - .orElseThrow(() -> new RuntimeException("Profile not found")); + public Timetable createTimetableForProfile(TimetableDto timetableDto, Principal principal) { + // Principal에서 사용자 이메일 추출 + String email = principal.getName(); + + // 이메일로 Profile 조회 + Profile profile = profileRepository.findByEmail(email) + .orElseThrow(() -> new AppException(ErrorCode.EMAIL_NOT_FOUND, email)); - // 새로운 Timetable 생성 + // Timetable 생성 Timetable timetable = Timetable.builder() - .profile(profile) // 프로필과 연결된 시간표 생성 + .profile(profile) // Profile 연관 + .name(timetableDto.getName()) // 요청 데이터에서 이름 설정 .build(); - // 시간표 저장 return timetableRepository.save(timetable); } - @Transactional - public void addCourseToTimetable(Long timetableId, Long courseId) { - Timetable timetable = timetableRepository.findById(timetableId) - .orElseThrow(() -> new RuntimeException("Timetable not found")); - - Course course = courseRepository.findById(courseId) - .orElseThrow(() -> new RuntimeException("Course not found")); - // Course와 Timetable 간의 연관 관계 추가 - if (!course.getTimetables().contains(timetable)) { - course.getTimetables().add(timetable); - } - if (!timetable.getCourses().contains(course)) { - timetable.getCourses().add(course); + /** 🟢 시간표에 강의 추가 */ + @Transactional + public ResponseEntity addCourseToTimetable(Long timetableId, Long courseId, Principal principal) { + Timetable timetable = validateProfileOwnership(timetableId, principal); + Course course = courseRepository.findById(courseId).orElse(null); + + if (course == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(ResponseDto.Error.builder() + .code("NOT_FOUND") + .message("해당 강의를 찾을 수 없습니다.") + .version("v1.1.4") + .build()); } - // 변경 사항 저장 - courseRepository.save(course); + timetable.getCourses().add(course); + course.getTimetables().add(timetable); + timetableRepository.save(timetable); + courseRepository.save(course); + + return ResponseEntity.status(HttpStatus.OK) + .body(ResponseDto.Success.builder() + .message("강의가 시간표에 추가되었습니다.") + .version("v1.1.4") + .build()); } + /** 🟢 시간표에서 강의 제거 */ @Transactional - public void removeCourseFromTimetable(Long timetableId, Long courseId) { - Timetable timetable = timetableRepository.findById(timetableId) - .orElseThrow(() -> new RuntimeException("Timetable not found")); - - Course course = courseRepository.findById(courseId) - .orElseThrow(() -> new RuntimeException("Course not found")); + public ResponseEntity removeCourseFromTimetable(Long timetableId, Long courseId, Principal principal) { + Timetable timetable = validateProfileOwnership(timetableId, principal); + Course course = courseRepository.findById(courseId).orElse(null); + + if (course == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(ResponseDto.Error.builder() + .code("NOT_FOUND") + .message("해당 강의를 찾을 수 없습니다.") + .version("v1.1.4") + .build()); + } - // Course와 Timetable 간의 연관 관계 제거 - course.getTimetables().remove(timetable); timetable.getCourses().remove(course); + course.getTimetables().remove(timetable); - // 변경 사항 저장 - courseRepository.save(course); timetableRepository.save(timetable); + courseRepository.save(course); + + return ResponseEntity.status(HttpStatus.OK) + .body(ResponseDto.Success.builder() + .message("강의가 시간표에서 제거되었습니다.") + .version("v1.1.4") + .build()); } + /** 🟢 시간표에 개인 일정 추가 */ @Transactional - public void addPrivacyToTimetable(Long timetableId, PrivacyDto privacyDto) { - Timetable timetable = timetableRepository.findById(timetableId) - .orElseThrow(() -> new RuntimeException("Timetable not found")); + public ResponseEntity addPrivacyToTimetable(Long timetableId, PrivacyDto privacyDto, Principal principal) { + Timetable timetable = validateProfileOwnership(timetableId, principal); Privacy privacy = Privacy.builder() - .timetable(timetable) .name(privacyDto.getName()) .day(privacyDto.getDay()) .startTime(privacyDto.getStartTime()) @@ -92,36 +121,27 @@ public void addPrivacyToTimetable(Long timetableId, PrivacyDto privacyDto) { .location(privacyDto.getLocation()) .build(); + privacy.getTimetables().add(timetable); privacyRepository.save(privacy); - } - @Transactional - public void removePrivacyFromTimetable(Long timetableId, Long privacyId) { - Privacy privacy = privacyRepository.findById(privacyId) - .orElseThrow(() -> new RuntimeException("Privacy not found")); - - // timetableId가 privacy의 timetables 리스트에 포함되어 있는지 검사 - boolean isLinkedToTimetable = privacy.getTimetables().stream() - .anyMatch(timetable -> timetable.getId().equals(timetableId)); - - if (!isLinkedToTimetable) { - throw new RuntimeException("Privacy does not belong to this Timetable"); - } - - // Timetable과 Privacy 간의 연관 관계 해제 - privacy.getTimetables().removeIf(timetable -> timetable.getId().equals(timetableId)); - privacyRepository.save(privacy); - } - - // profileId로 timetable 조회 - public List getTimetableByProfileId(Long profileId) { - List timetables = timetableRepository.findByProfile_profileId(profileId); + return ResponseEntity.status(HttpStatus.OK) + .body(ResponseDto.Success.builder() + .message("개인 일정이 시간표에 추가되었습니다.") + .data(privacy) + .version("v1.1.4") + .build()); + } - // timetable이 없다면 적절한 메시지를 반환 - if (timetables.isEmpty()) { - throw new RuntimeException("해당 프로필에 대한 timetable이 없습니다. 새로 만들어보세요!"); - } + /** 🔹 Helper Method: Principal을 이용해 Profile 조회 */ + private Profile getProfileByPrincipal(Principal principal) { + return profileRepository.findByEmail(principal.getName()) + .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); + } - return timetables; + /** 🔹 Helper Method: 주어진 시간표 ID가 현재 로그인된 사용자의 것인지 검증 */ + private Timetable validateProfileOwnership(Long timetableId, Principal principal) { + return timetableRepository.findById(timetableId) + .filter(timetable -> timetable.getProfile().getEmail().equals(principal.getName())) + .orElseThrow(() -> new RuntimeException("접근 권한이 없습니다.")); } } From 73a8b45b31d06518c8ad52956ea660c45632202e Mon Sep 17 00:00:00 2001 From: kimyerak Date: Wed, 29 Jan 2025 03:03:59 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20timetable=20api=204=EA=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/TimetableController.java | 69 ++++++-- .../Devkor_project/dto/TimetableDto.java | 3 + .../Devkor_project/exception/ErrorCode.java | 5 +- .../service/TimetableService.java | 153 +++++++++++++----- 4 files changed, 181 insertions(+), 49 deletions(-) diff --git a/src/main/java/com/example/Devkor_project/controller/TimetableController.java b/src/main/java/com/example/Devkor_project/controller/TimetableController.java index ae1a134..c3e2ea0 100644 --- a/src/main/java/com/example/Devkor_project/controller/TimetableController.java +++ b/src/main/java/com/example/Devkor_project/controller/TimetableController.java @@ -45,28 +45,77 @@ public ResponseEntity createTimetable( .build()); } - /** 🟢 강의 추가 */ - @PostMapping("/{id}/course") + @PostMapping("/{timetableId}/course") @Operation(summary = "시간표에 강의 추가") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") - public ResponseEntity addCourseToTimetable(@PathVariable Long id, @RequestParam Long courseId, Principal principal) { - return timetableService.addCourseToTimetable(id, courseId, principal); + public ResponseEntity addCourseToTimetable(@PathVariable Long timetableId, @RequestParam Long courseId, Principal principal) { + return timetableService.addCourseToTimetable(timetableId, courseId, principal); } /** 🟢 강의 제거 */ - @DeleteMapping("/{id}/course/{courseId}") + @DeleteMapping("/{timetableId}/course/{courseId}") @Operation(summary = "시간표에서 강의 제거") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") - public ResponseEntity removeCourseFromTimetable(@PathVariable Long id, @PathVariable Long courseId, Principal principal) { - return timetableService.removeCourseFromTimetable(id, courseId, principal); + public ResponseEntity removeCourseFromTimetable(@PathVariable Long timetableId, @PathVariable Long courseId, Principal principal) { + return timetableService.removeCourseFromTimetable(timetableId, courseId, principal); } /** 🟢 개인 일정 추가 */ - @PostMapping("/{id}/privacy") + @PostMapping("/{timetableId}/privacy") @Operation(summary = "시간표에 개인 일정 추가") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") - public ResponseEntity addPrivacyToTimetable(@PathVariable Long id, @Valid @RequestBody PrivacyDto privacyDto, Principal principal) { - return timetableService.addPrivacyToTimetable(id, privacyDto, principal); + public ResponseEntity addPrivacyToTimetable(@PathVariable Long timetableId, @Valid @RequestBody PrivacyDto privacyDto, Principal principal) { + return timetableService.addPrivacyToTimetable(timetableId, privacyDto, principal); + } + + /** 🟢 로그인된 사용자의 모든 시간표 조회 */ + @GetMapping + @Operation(summary = "로그인된 사용자의 모든 시간표 조회") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + + public ResponseEntity getAllTimetablesForUser(Principal principal) { + return timetableService.getAllTimetablesForUser(principal); + } + + /** 🟢 로그인된 사용자의 시간표 이름과 ID 조회 */ + @GetMapping("/names-and-ids") + @Operation(summary = "로그인된 사용자의 시간표 이름과 ID 조회") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + public ResponseEntity getTimetableNamesAndIds(Principal principal) { + return timetableService.getTimetableNamesAndIds(principal); + } + + /** 🟢 특정 시간표 조회 */ + @GetMapping("/{timetableId}") + @Operation(summary = "특정 시간표 조회") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + public ResponseEntity getTimetableById(@PathVariable Long timetableId, Principal principal) { + return timetableService.getTimetableById(timetableId, principal); + } + + /** 🟢 시간표에서 개인 일정 제거 */ + @DeleteMapping("/{timetableId}/privacy/{privacyId}") + @Operation(summary = "시간표에서 개인 일정 제거") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + public ResponseEntity removePrivacyFromTimetable( + @PathVariable Long timetableId, + @PathVariable Long privacyId, + Principal principal + ) { + return timetableService.removePrivacyFromTimetable(timetableId, privacyId, principal); + } + + /** 🟢 시간표 이름 변경 */ + @PutMapping("/{timetableId}") + @Operation(summary = "시간표 이름 변경") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + @Parameter(in = ParameterIn.PATH, name = "timetableId", description = "이름을 변경할 시간표 ID") + public ResponseEntity updateTimetableName( + @PathVariable Long timetableId, + @RequestBody TimetableDto timetableDto, + Principal principal + ) { + return timetableService.updateTimetableName(timetableId, timetableDto, principal); } } diff --git a/src/main/java/com/example/Devkor_project/dto/TimetableDto.java b/src/main/java/com/example/Devkor_project/dto/TimetableDto.java index 3d6d2d8..7979b88 100644 --- a/src/main/java/com/example/Devkor_project/dto/TimetableDto.java +++ b/src/main/java/com/example/Devkor_project/dto/TimetableDto.java @@ -14,6 +14,9 @@ @Builder @Schema(description = "응답 성공 DTO") public class TimetableDto { + @NotBlank(message = "[profileId] cannot be blank.") + @Schema(description = "profile ID") + private Long profileId; @Schema(description = "Timetable ID") private Long id; diff --git a/src/main/java/com/example/Devkor_project/exception/ErrorCode.java b/src/main/java/com/example/Devkor_project/exception/ErrorCode.java index 9493bd3..ab10c89 100644 --- a/src/main/java/com/example/Devkor_project/exception/ErrorCode.java +++ b/src/main/java/com/example/Devkor_project/exception/ErrorCode.java @@ -42,8 +42,9 @@ public enum ErrorCode TRAFFIC_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 기간 동안 요청이 들어오지 않았습니다."), UNKNOWN_NOT_FOUND(HttpStatus.NOT_FOUND, "알 수 없음 계정이 존재하지 않습니다."), INVALID_PERIOD(HttpStatus.BAD_REQUEST, "해당 교시는 유효하지 않습니다."), - INVALID_TIME_LOCATION(HttpStatus.BAD_REQUEST, "해당 시간 및 장소 정보는 유효하지 않습니다."); - + INVALID_TIME_LOCATION(HttpStatus.BAD_REQUEST, "해당 시간 및 장소 정보는 유효하지 않습니다."), + TIMETABLE_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 시간표를 찾을 수 없습니다."), + PRIVACY_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 개인 일정을 찾을 수 없습니다."); private final HttpStatus httpStatus; private final String message; } diff --git a/src/main/java/com/example/Devkor_project/service/TimetableService.java b/src/main/java/com/example/Devkor_project/service/TimetableService.java index 014e97a..78d26d2 100644 --- a/src/main/java/com/example/Devkor_project/service/TimetableService.java +++ b/src/main/java/com/example/Devkor_project/service/TimetableService.java @@ -32,40 +32,81 @@ public class TimetableService { private final PrivacyRepository privacyRepository; private final ProfileRepository profileRepository; - /** 🟢 로그인된 사용자의 시간표 생성 */ + /** + * 로그인된 사용자의 시간표 생성 + */ @Transactional public Timetable createTimetableForProfile(TimetableDto timetableDto, Principal principal) { - // Principal에서 사용자 이메일 추출 - String email = principal.getName(); - - // 이메일로 Profile 조회 - Profile profile = profileRepository.findByEmail(email) - .orElseThrow(() -> new AppException(ErrorCode.EMAIL_NOT_FOUND, email)); + Profile profile = getProfileByPrincipal(principal); // Timetable 생성 Timetable timetable = Timetable.builder() - .profile(profile) // Profile 연관 - .name(timetableDto.getName()) // 요청 데이터에서 이름 설정 + .profile(profile) + .name(timetableDto.getName()) .build(); return timetableRepository.save(timetable); } + /** + * 로그인된 사용자의 모든 시간표 조회 + */ + @Transactional + public ResponseEntity getAllTimetablesForUser(Principal principal) { + Profile profile = getProfileByPrincipal(principal); + + List timetables = timetableRepository.findByProfile_profileId(profile.getProfile_id()); + + return ResponseEntity.status(HttpStatus.OK) + .body(ResponseDto.Success.builder() + .message("모든 시간표를 성공적으로 조회하였습니다.") + .data(timetables) + .version("v1.1.4") + .build()); + } + + /** + * 로그인된 사용자의 시간표 이름과 ID 조회 + */ + @Transactional + public ResponseEntity getTimetableNamesAndIds(Principal principal) { + Profile profile = getProfileByPrincipal(principal); + + List timetableDtos = timetableRepository.findByProfile_profileId(profile.getProfile_id()).stream() + .map(t -> new TimetableDto(null, t.getId(), t.getName())) + .collect(Collectors.toList()); + + return ResponseEntity.status(HttpStatus.OK) + .body(ResponseDto.Success.builder() + .message("시간표 이름과 ID를 성공적으로 조회하였습니다.") + .data(timetableDtos) + .version("v1.1.4") + .build()); + } - /** 🟢 시간표에 강의 추가 */ + /** + * 특정 시간표 조회 + */ @Transactional - public ResponseEntity addCourseToTimetable(Long timetableId, Long courseId, Principal principal) { + public ResponseEntity getTimetableById(Long timetableId, Principal principal) { Timetable timetable = validateProfileOwnership(timetableId, principal); - Course course = courseRepository.findById(courseId).orElse(null); - if (course == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ResponseDto.Error.builder() - .code("NOT_FOUND") - .message("해당 강의를 찾을 수 없습니다.") - .version("v1.1.4") - .build()); - } + return ResponseEntity.status(HttpStatus.OK) + .body(ResponseDto.Success.builder() + .message("특정 시간표를 성공적으로 조회하였습니다.") + .data(timetable) + .version("v1.1.4") + .build()); + } + + /** + * 시간표에 강의 추가 + */ + @Transactional + public ResponseEntity addCourseToTimetable(Long timetableId, Long courseId, Principal principal) { + Timetable timetable = validateProfileOwnership(timetableId, principal); + Course course = courseRepository.findById(courseId) + .orElseThrow(() -> new AppException(ErrorCode.COURSE_NOT_FOUND, "해당 강의를 찾을 수 없습니다.")); timetable.getCourses().add(course); course.getTimetables().add(timetable); @@ -80,20 +121,14 @@ public ResponseEntity addCourseToTimetable(Long timetableId, Long courseId, P .build()); } - /** 🟢 시간표에서 강의 제거 */ + /** + * 시간표에서 강의 제거 + */ @Transactional - public ResponseEntity removeCourseFromTimetable(Long timetableId, Long courseId, Principal principal) { + public ResponseEntity removeCourseFromTimetable(Long timetableId, Long courseId, Principal principal) { Timetable timetable = validateProfileOwnership(timetableId, principal); - Course course = courseRepository.findById(courseId).orElse(null); - - if (course == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ResponseDto.Error.builder() - .code("NOT_FOUND") - .message("해당 강의를 찾을 수 없습니다.") - .version("v1.1.4") - .build()); - } + Course course = courseRepository.findById(courseId) + .orElseThrow(() -> new AppException(ErrorCode.COURSE_NOT_FOUND, "해당 강의를 찾을 수 없습니다.")); timetable.getCourses().remove(course); course.getTimetables().remove(timetable); @@ -108,7 +143,9 @@ public ResponseEntity removeCourseFromTimetable(Long timetableId, Long course .build()); } - /** 🟢 시간표에 개인 일정 추가 */ + /** + * 시간표에 개인 일정 추가 + */ @Transactional public ResponseEntity addPrivacyToTimetable(Long timetableId, PrivacyDto privacyDto, Principal principal) { Timetable timetable = validateProfileOwnership(timetableId, principal); @@ -132,16 +169,58 @@ public ResponseEntity addPrivacyToTimetable(Long timetableI .build()); } - /** 🔹 Helper Method: Principal을 이용해 Profile 조회 */ + /** + * 시간표에서 개인 일정 제거 + */ + @Transactional + public ResponseEntity removePrivacyFromTimetable(Long timetableId, Long privacyId, Principal principal) { + Timetable timetable = validateProfileOwnership(timetableId, principal); + + Privacy privacy = privacyRepository.findById(privacyId) + .orElseThrow(() -> new AppException(ErrorCode.TIMETABLE_NOT_FOUND, "해당 개인 일정을 찾을 수 없습니다.")); + + timetable.getPrivacies().remove(privacy); + privacy.getTimetables().remove(timetable); + + timetableRepository.save(timetable); + privacyRepository.save(privacy); + + return ResponseEntity.status(HttpStatus.OK) + .body(ResponseDto.Success.builder() + .message("시간표에서 개인 일정이 성공적으로 제거되었습니다.") + .version("v1.1.4") + .build()); + } + @Transactional + public ResponseEntity updateTimetableName(Long timetableId, TimetableDto timetableDto, Principal principal) { + Timetable timetable = validateProfileOwnership(timetableId, principal); + + // 이름 변경 + timetable.setName(timetableDto.getName()); + timetableRepository.save(timetable); + + return ResponseEntity.status(HttpStatus.OK) + .body(ResponseDto.Success.builder() + .message("시간표 이름이 성공적으로 변경되었습니다.") + .data(timetable) + .version("v1.1.4") + .build()); + } + + /** + * Helper Method: Principal을 이용해 Profile 조회 + */ private Profile getProfileByPrincipal(Principal principal) { return profileRepository.findByEmail(principal.getName()) - .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); + .orElseThrow(() -> new AppException(ErrorCode.EMAIL_NOT_FOUND, "사용자를 찾을 수 없습니다.")); } - /** 🔹 Helper Method: 주어진 시간표 ID가 현재 로그인된 사용자의 것인지 검증 */ + /** + * Helper Method: 주어진 시간표 ID가 현재 로그인된 사용자의 것인지 검증 + */ private Timetable validateProfileOwnership(Long timetableId, Principal principal) { return timetableRepository.findById(timetableId) .filter(timetable -> timetable.getProfile().getEmail().equals(principal.getName())) - .orElseThrow(() -> new RuntimeException("접근 권한이 없습니다.")); + .orElseThrow(() -> new AppException(ErrorCode.INVALID_TOKEN, "접근 권한이 없습니다.")); } } From 2e33d2df58b5270b1419316c808d10ce48c459d8 Mon Sep 17 00:00:00 2001 From: YunJaeHoon Date: Sun, 2 Feb 2025 02:10:48 +0900 Subject: [PATCH 04/11] =?UTF-8?q?FTR:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../Devkor_project/dto/ProfileDto.java | 2 + .../repository/BookmarkRepository.java | 2 + .../repository/CodeRepository.java | 2 + .../repository/CommentLikeRepository.java | 2 + .../repository/CommentRatingRepository.java | 2 + .../repository/CommentReportRepository.java | 2 + .../repository/CommentRepository.java | 2 + .../repository/CourseRatingRepository.java | 2 + .../repository/CourseRepository.java | 2 + .../repository/ProfileRepository.java | 2 + .../repository/TimeLocationRepository.java | 2 + .../repository/TrafficRepository.java | 2 + .../security/CustomUserDetailsService.java | 7 +- .../service/LoginServiceTest.java | 278 ++++++++++++++++++ 15 files changed, 306 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/example/Devkor_project/service/LoginServiceTest.java diff --git a/build.gradle b/build.gradle index e0fd1c0..e7eb187 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,7 @@ dependencies { // 메일 서버 연결 라이브러리 implementation 'org.springframework.boot:spring-boot-starter-mail' - // Jwt + // JWT implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' diff --git a/src/main/java/com/example/Devkor_project/dto/ProfileDto.java b/src/main/java/com/example/Devkor_project/dto/ProfileDto.java index 8b2253c..f75e2d8 100644 --- a/src/main/java/com/example/Devkor_project/dto/ProfileDto.java +++ b/src/main/java/com/example/Devkor_project/dto/ProfileDto.java @@ -68,6 +68,8 @@ public static class Profile @AllArgsConstructor @ToString @Getter + @Setter + @Builder public static class Signup { @NotBlank(message = "[email] cannot be blank.") diff --git a/src/main/java/com/example/Devkor_project/repository/BookmarkRepository.java b/src/main/java/com/example/Devkor_project/repository/BookmarkRepository.java index d1e2710..b2252ab 100644 --- a/src/main/java/com/example/Devkor_project/repository/BookmarkRepository.java +++ b/src/main/java/com/example/Devkor_project/repository/BookmarkRepository.java @@ -7,9 +7,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import java.util.List; +@Repository public interface BookmarkRepository extends JpaRepository { @Query(value = "SELECT * FROM bookmark WHERE profile_id = :profile_id AND course_id = :course_id", nativeQuery = true) diff --git a/src/main/java/com/example/Devkor_project/repository/CodeRepository.java b/src/main/java/com/example/Devkor_project/repository/CodeRepository.java index 562675c..a962a54 100644 --- a/src/main/java/com/example/Devkor_project/repository/CodeRepository.java +++ b/src/main/java/com/example/Devkor_project/repository/CodeRepository.java @@ -4,10 +4,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; import java.time.LocalDate; import java.util.Optional; +@Repository public interface CodeRepository extends JpaRepository { Optional findByEmail(String email); diff --git a/src/main/java/com/example/Devkor_project/repository/CommentLikeRepository.java b/src/main/java/com/example/Devkor_project/repository/CommentLikeRepository.java index f8ee0cf..782c5ca 100644 --- a/src/main/java/com/example/Devkor_project/repository/CommentLikeRepository.java +++ b/src/main/java/com/example/Devkor_project/repository/CommentLikeRepository.java @@ -5,9 +5,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import java.util.List; +@Repository public interface CommentLikeRepository extends JpaRepository { // profile_id, comment_id로 CommentLike 조회 diff --git a/src/main/java/com/example/Devkor_project/repository/CommentRatingRepository.java b/src/main/java/com/example/Devkor_project/repository/CommentRatingRepository.java index 47926cc..b84df89 100644 --- a/src/main/java/com/example/Devkor_project/repository/CommentRatingRepository.java +++ b/src/main/java/com/example/Devkor_project/repository/CommentRatingRepository.java @@ -2,6 +2,8 @@ import com.example.Devkor_project.entity.CommentRating; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +@Repository public interface CommentRatingRepository extends JpaRepository { } diff --git a/src/main/java/com/example/Devkor_project/repository/CommentReportRepository.java b/src/main/java/com/example/Devkor_project/repository/CommentReportRepository.java index 65412c0..2e8e32b 100644 --- a/src/main/java/com/example/Devkor_project/repository/CommentReportRepository.java +++ b/src/main/java/com/example/Devkor_project/repository/CommentReportRepository.java @@ -6,9 +6,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import java.util.List; +@Repository public interface CommentReportRepository extends JpaRepository { // 모든 CommentReport 개수 diff --git a/src/main/java/com/example/Devkor_project/repository/CommentRepository.java b/src/main/java/com/example/Devkor_project/repository/CommentRepository.java index d089d64..cd07dec 100644 --- a/src/main/java/com/example/Devkor_project/repository/CommentRepository.java +++ b/src/main/java/com/example/Devkor_project/repository/CommentRepository.java @@ -9,10 +9,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; +@Repository public interface CommentRepository extends JpaRepository { @Query(value = "SELECT * FROM comment WHERE profile_id = :profile_id AND course_id = :course_id", nativeQuery = true) diff --git a/src/main/java/com/example/Devkor_project/repository/CourseRatingRepository.java b/src/main/java/com/example/Devkor_project/repository/CourseRatingRepository.java index 140e9a5..7303dc7 100644 --- a/src/main/java/com/example/Devkor_project/repository/CourseRatingRepository.java +++ b/src/main/java/com/example/Devkor_project/repository/CourseRatingRepository.java @@ -2,6 +2,8 @@ import com.example.Devkor_project.entity.CourseRating; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +@Repository public interface CourseRatingRepository extends JpaRepository { } diff --git a/src/main/java/com/example/Devkor_project/repository/CourseRepository.java b/src/main/java/com/example/Devkor_project/repository/CourseRepository.java index 0cc5df0..b4ec9b1 100644 --- a/src/main/java/com/example/Devkor_project/repository/CourseRepository.java +++ b/src/main/java/com/example/Devkor_project/repository/CourseRepository.java @@ -7,11 +7,13 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import java.time.LocalDate; import java.util.List; import java.util.Optional; +@Repository public interface CourseRepository extends JpaRepository { @Query(value = "SELECT * FROM course WHERE name LIKE %:word% OR professor LIKE %:word% OR course_code LIKE %:word% ORDER BY year DESC, semester DESC, course_id DESC", diff --git a/src/main/java/com/example/Devkor_project/repository/ProfileRepository.java b/src/main/java/com/example/Devkor_project/repository/ProfileRepository.java index 224b402..a56a01f 100644 --- a/src/main/java/com/example/Devkor_project/repository/ProfileRepository.java +++ b/src/main/java/com/example/Devkor_project/repository/ProfileRepository.java @@ -4,9 +4,11 @@ import io.lettuce.core.dynamic.annotation.Param; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; import java.util.Optional; +@Repository public interface ProfileRepository extends JpaRepository { Optional findByEmail(String email); diff --git a/src/main/java/com/example/Devkor_project/repository/TimeLocationRepository.java b/src/main/java/com/example/Devkor_project/repository/TimeLocationRepository.java index 4440ad2..eb9b9d6 100644 --- a/src/main/java/com/example/Devkor_project/repository/TimeLocationRepository.java +++ b/src/main/java/com/example/Devkor_project/repository/TimeLocationRepository.java @@ -5,9 +5,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import java.util.List; +@Repository public interface TimeLocationRepository extends JpaRepository { @Query(value = "SELECT * FROM time_location WHERE course_id = :course_id", nativeQuery = true) diff --git a/src/main/java/com/example/Devkor_project/repository/TrafficRepository.java b/src/main/java/com/example/Devkor_project/repository/TrafficRepository.java index d7be1ec..7f24987 100644 --- a/src/main/java/com/example/Devkor_project/repository/TrafficRepository.java +++ b/src/main/java/com/example/Devkor_project/repository/TrafficRepository.java @@ -4,9 +4,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import java.util.List; +@Repository public interface TrafficRepository extends JpaRepository { @Query(value = "SELECT * FROM traffic WHERE api_path = :api_path AND year = :year AND month = :month", nativeQuery = true) diff --git a/src/main/java/com/example/Devkor_project/security/CustomUserDetailsService.java b/src/main/java/com/example/Devkor_project/security/CustomUserDetailsService.java index 633be5a..1bc189c 100644 --- a/src/main/java/com/example/Devkor_project/security/CustomUserDetailsService.java +++ b/src/main/java/com/example/Devkor_project/security/CustomUserDetailsService.java @@ -18,10 +18,9 @@ @Service @Transactional(readOnly = true) @RequiredArgsConstructor -public class CustomUserDetailsService implements UserDetailsService { - - @Autowired - private ProfileRepository profileRepository; +public class CustomUserDetailsService implements UserDetailsService +{ + private final ProfileRepository profileRepository; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { diff --git a/src/test/java/com/example/Devkor_project/service/LoginServiceTest.java b/src/test/java/com/example/Devkor_project/service/LoginServiceTest.java new file mode 100644 index 0000000..07cf623 --- /dev/null +++ b/src/test/java/com/example/Devkor_project/service/LoginServiceTest.java @@ -0,0 +1,278 @@ +package com.example.Devkor_project.service; + +import com.example.Devkor_project.dto.ProfileDto; +import com.example.Devkor_project.entity.Code; +import com.example.Devkor_project.entity.Profile; +import com.example.Devkor_project.exception.AppException; +import com.example.Devkor_project.exception.ErrorCode; +import com.example.Devkor_project.repository.CodeRepository; +import com.example.Devkor_project.repository.ProfileRepository; +import com.example.Devkor_project.security.CustomUserDetailsService; +import com.example.Devkor_project.security.JwtUtil; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class LoginServiceTest +{ + @InjectMocks + private LoginService loginService; + + @Mock private ProfileRepository profileRepository; + @Mock private CodeRepository codeRepository; + @Spy private BCryptPasswordEncoder encoder; + @Mock private JavaMailSender javaMailSender; + @Mock private JwtUtil jwtUtil; + @Mock private CustomUserDetailsService customUserDetailService; + @Mock private RedisTemplate redisTemplate; + + private ProfileDto.Signup profileDto; + private Code code; + + /* 테스트 별 초기 설정 */ + @BeforeEach + void conditionalSetUp(TestInfo testInfo) + { + if ( + testInfo.getDisplayName().startsWith("회원가입 성공") || + testInfo.getDisplayName().startsWith("회원가입 실패") + ) + { + profileDto = ProfileDto.Signup.builder() + .email("test1234@korea.ac.kr") + .password("test1234") + .username("test1234") + .student_id("1234567") + .degree("MASTER") + .semester(1) + .department("test") + .code("12345678") + .build(); + + code = Code.builder() + .code_id(1L) + .email("test1234@korea.ac.kr") + .code("12345678") + .created_at(LocalDate.now()) + .build(); + } + } + + @Test + @DisplayName("회원가입 성공") + void signUp_success() + { + // Given + given(codeRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.of(code)); + given(profileRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.empty()); + given(profileRepository.findByUsername(profileDto.getUsername())) + .willReturn(Optional.empty()); + + // When + loginService.signUp(profileDto); + + // Then + verify(profileRepository, times(1)).save(any(Profile.class)); + } + + @Test + @DisplayName("회원가입 실패 1: 해당 이메일로 발송된 인증번호가 존재하지 않음") + void signUp_fail_1() + { + // Given + given(codeRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.empty()); + + // When & Then + AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); + assertEquals(ErrorCode.CODE_NOT_FOUND, exception.getErrorCode()); + + verify(profileRepository, times(0)).save(any(Profile.class)); + } + + @Test + @DisplayName("회원가입 실패 2: 인증번호가 틀림") + void signUp_fail_2() + { + // Given + profileDto.setCode("wrong_code"); + + given(codeRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.of(code)); + + // When & Then + AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); + assertEquals(ErrorCode.WRONG_CODE, exception.getErrorCode()); + + verify(profileRepository, times(0)).save(any(Profile.class)); + } + + @Test + @DisplayName("회원가입 실패 3: 이메일 중복") + void signUp_fail_3() + { + // Given + given(codeRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.of(code)); + given(profileRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.of(new Profile())); + + // When & Then + AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); + assertEquals(ErrorCode.EMAIL_DUPLICATED, exception.getErrorCode()); + + verify(profileRepository, times(0)).save(any(Profile.class)); + } + + @Test + @DisplayName("회원가입 실패 4: 학번 형식 오류") + void signUp_fail_4() + { + // Given + profileDto.setStudent_id("12345678"); + + given(codeRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.of(code)); + given(profileRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.empty()); + + // When & Then + AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); + assertEquals(ErrorCode.INVALID_STUDENT_ID, exception.getErrorCode()); + + verify(profileRepository, times(0)).save(any(Profile.class)); + } + + @ParameterizedTest(name = "회원가입 실패 5: 비밀번호가 {0}") + @CsvSource({ + "test", + "test012345678901234567890123456789", + "0123456789", + "abcdefghij" + }) + @DisplayName("회원가입 실패 5: 비밀번호 형식 오류") + void signUp_fail_5(String password) + { + // Given + profileDto.setPassword(password); + + given(codeRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.of(code)); + given(profileRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.empty()); + + // When & Then + AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); + assertEquals(ErrorCode.INVALID_PASSWORD, exception.getErrorCode()); + + verify(profileRepository, times(0)).save(any(Profile.class)); + } + + @Test + @DisplayName("회원가입 실패 6: 닉네임 형식 오류") + void signUp_fail_6() + { + // Given + profileDto.setUsername("01234567890123456789"); + + given(codeRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.of(code)); + given(profileRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.empty()); + + // When & Then + AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); + assertEquals(ErrorCode.INVALID_USERNAME, exception.getErrorCode()); + + verify(profileRepository, times(0)).save(any(Profile.class)); + } + + @Test + @DisplayName("회원가입 실패 7: 닉네임 중복") + void signUp_fail_7() + { + // Given + given(codeRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.of(code)); + given(profileRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.empty()); + given(profileRepository.findByUsername(profileDto.getUsername())) + .willReturn(Optional.of(new Profile())); + + // When & Then + AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); + assertEquals(ErrorCode.USERNAME_DUPLICATED, exception.getErrorCode()); + + verify(profileRepository, times(0)).save(any(Profile.class)); + } + + @Test + @DisplayName("회원가입 실패 8: 학위 형식 오류") + void signUp_fail_8() + { + // Given + profileDto.setDegree("wrong_degree"); + + given(codeRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.of(code)); + given(profileRepository.findByEmail(profileDto.getEmail())) + .willReturn(Optional.empty()); + given(profileRepository.findByUsername(profileDto.getUsername())) + .willReturn(Optional.empty()); + + // When & Then + AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); + assertEquals(ErrorCode.INVALID_DEGREE, exception.getErrorCode()); + + verify(profileRepository, times(0)).save(any(Profile.class)); + } + + @Test + void sendAuthenticationNumber() { + } + + @Test + void checkAuthenticationNumber() { + } + + @Test + void checkUsername() { + } + + @Test + void resetPassword() { + } + + @Test + void checkLogin() { + } + + @Test + void refreshToken() { + } +} \ No newline at end of file From 2d165f025270fd8164a3f3f3b02185b13b9e4d04 Mon Sep 17 00:00:00 2001 From: YunJaeHoon Date: Sun, 2 Feb 2025 17:38:34 +0900 Subject: [PATCH 05/11] =?UTF-8?q?FIX:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevkorProjectApplication.java | 1 - .../Devkor_project/service/LoginService.java | 28 +- .../DevkorProjectApplication.java | 23 - .../DevkorProjectApplicationTests.java | 13 + .../service/LoginServiceTest.java | 429 ++++++++++++------ 5 files changed, 309 insertions(+), 185 deletions(-) delete mode 100644 src/test/java/com/example/Devkor_project/DevkorProjectApplication.java create mode 100644 src/test/java/com/example/Devkor_project/DevkorProjectApplicationTests.java diff --git a/src/main/java/com/example/Devkor_project/DevkorProjectApplication.java b/src/main/java/com/example/Devkor_project/DevkorProjectApplication.java index ffdcf88..d9b7225 100644 --- a/src/main/java/com/example/Devkor_project/DevkorProjectApplication.java +++ b/src/main/java/com/example/Devkor_project/DevkorProjectApplication.java @@ -10,7 +10,6 @@ @SpringBootApplication @EnableScheduling // SchedulerService를 위한 Annotation -@EnableAsync // 비동기 처리 허용 public class DevkorProjectApplication { public static void main(String[] args) { diff --git a/src/main/java/com/example/Devkor_project/service/LoginService.java b/src/main/java/com/example/Devkor_project/service/LoginService.java index cf08ae9..c632937 100644 --- a/src/main/java/com/example/Devkor_project/service/LoginService.java +++ b/src/main/java/com/example/Devkor_project/service/LoginService.java @@ -27,11 +27,11 @@ import java.security.Principal; import java.time.LocalDate; import java.util.Objects; +import java.util.Optional; import java.util.Random; @Service @RequiredArgsConstructor -@Slf4j public class LoginService { private final ProfileRepository profileRepository; @@ -46,15 +46,15 @@ public class LoginService @Transactional public void signUp(ProfileDto.Signup dto) { - // 해당 이메일로 발송된 인증번호가 있는지 체크 + // 해당 이메일로 발송된 인증번호가 없는 경우, 예외 처리 Code code = codeRepository.findByEmail(dto.getEmail()) .orElseThrow(() -> new AppException(ErrorCode.CODE_NOT_FOUND, dto.getEmail())); - // 입력한 인증번호가 맞는지 체크 + // 입력한 인증번호가 틀린 경우, 예외 처리 if(!Objects.equals(dto.getCode(), code.getCode())) throw new AppException(ErrorCode.WRONG_CODE, dto.getEmail()); - // 이메일 중복 체크 + // 이메일이 중복된 경우, 예외 처리 profileRepository.findByEmail(dto.getEmail()) .ifPresent(user -> { throw new AppException(ErrorCode.EMAIL_DUPLICATED, dto.getEmail()); @@ -94,7 +94,7 @@ public void signUp(ProfileDto.Signup dto) if(dto.getUsername().isEmpty() || dto.getUsername().length() > 10) throw new AppException(ErrorCode.INVALID_USERNAME, dto.getUsername()); - // 닉네임 중복 체크 + // 닉네임이 중복된 경우, 예외 처리 profileRepository.findByUsername(dto.getUsername()) .ifPresent(user -> { throw new AppException(ErrorCode.USERNAME_DUPLICATED, dto.getUsername()); @@ -127,10 +127,11 @@ public void signUp(ProfileDto.Signup dto) @Transactional public void sendAuthenticationNumber(String email, String purpose) { - // 고려대 이메일인지 확인 + // 고려대 이메일이 아닌 경우, 예외 처리 if(!email.endsWith("@korea.ac.kr")) throw new AppException(ErrorCode.EMAIL_NOT_KOREA, email); + // purpose 형식이 잘못된 경우, 예외 처리 if(!purpose.equals("SIGN_UP") && !purpose.equals("RESET_PASSWORD")) throw new AppException(ErrorCode.INVALID_PURPOSE, purpose); @@ -213,12 +214,11 @@ else if(purpose.equals("RESET_PASSWORD") && profileRepository.findByEmail(email) ); // 이미 인증번호가 발송된 이메일인 경우, 데이터베이스에서 인증번호 정보 삭제 - Code code = codeRepository.findByEmail(email).orElse(null); - if(code != null) - codeRepository.delete(code); + Optional existingCode = codeRepository.findByEmail(email); + existingCode.ifPresent(codeRepository::delete); // 인증번호를 데이터베이스에 저장 - code = Code.builder() + Code code = Code.builder() .email(email) .code(authenticationNumber) .created_at(LocalDate.now()) @@ -247,19 +247,19 @@ else if(purpose.equals("RESET_PASSWORD") && profileRepository.findByEmail(email) @Transactional public void checkAuthenticationNumber(String email, String code) { - // 고려대 이메일인지 확인 + // 고려대 이메일이 아닌 경우, 예외 처리 if (!email.endsWith("@korea.ac.kr")) throw new AppException(ErrorCode.EMAIL_NOT_KOREA, email); - // 해당 이메일로 발송된 인증번호가 있는지 체크 + // 해당 이메일로 발송된 인증번호가 없는 경우, 예외 처리 Code actualCode = codeRepository.findByEmail(email) .orElseThrow(() -> new AppException(ErrorCode.CODE_NOT_FOUND, email)); - // 입력한 인증번호가 맞는지 체크 + // 입력한 인증번호가 틀린 경우, 예외 처리 if(!Objects.equals(code, actualCode.getCode())) throw new AppException(ErrorCode.WRONG_CODE, email); - // 이메일 중복 체크 + // 이메일 중복된 경우, 예외 처리 profileRepository.findByEmail(email) .ifPresent(user -> { throw new AppException(ErrorCode.EMAIL_DUPLICATED, email); diff --git a/src/test/java/com/example/Devkor_project/DevkorProjectApplication.java b/src/test/java/com/example/Devkor_project/DevkorProjectApplication.java deleted file mode 100644 index bc20281..0000000 --- a/src/test/java/com/example/Devkor_project/DevkorProjectApplication.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.Devkor_project; - -// import com.example.Devkor_project.controller.LoginController; -// import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -// import org.mockito.Mock; -// import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -// import org.springframework.test.web.servlet.MockMvc; -// import org.springframework.test.web.servlet.RequestBuilder; -// import org.springframework.test.web.servlet.setup.MockMvcBuilders; - -// import static -// org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; - -@SpringBootTest -class DevkorProjectApplication { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/com/example/Devkor_project/DevkorProjectApplicationTests.java b/src/test/java/com/example/Devkor_project/DevkorProjectApplicationTests.java new file mode 100644 index 0000000..c5157e9 --- /dev/null +++ b/src/test/java/com/example/Devkor_project/DevkorProjectApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.Devkor_project; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DevkorProjectApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/com/example/Devkor_project/service/LoginServiceTest.java b/src/test/java/com/example/Devkor_project/service/LoginServiceTest.java index 07cf623..264ac70 100644 --- a/src/test/java/com/example/Devkor_project/service/LoginServiceTest.java +++ b/src/test/java/com/example/Devkor_project/service/LoginServiceTest.java @@ -1,66 +1,55 @@ package com.example.Devkor_project.service; import com.example.Devkor_project.dto.ProfileDto; -import com.example.Devkor_project.entity.Code; import com.example.Devkor_project.entity.Profile; import com.example.Devkor_project.exception.AppException; import com.example.Devkor_project.exception.ErrorCode; import com.example.Devkor_project.repository.CodeRepository; import com.example.Devkor_project.repository.ProfileRepository; -import com.example.Devkor_project.security.CustomUserDetailsService; -import com.example.Devkor_project.security.JwtUtil; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; +import jakarta.mail.internet.MimeMessage; import org.junit.jupiter.api.*; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.BDDMockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.util.Optional; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.*; -@ExtendWith(MockitoExtension.class) +@SpringBootTest +@Transactional class LoginServiceTest { - @InjectMocks - private LoginService loginService; + @Autowired(required = false) LoginService loginService; - @Mock private ProfileRepository profileRepository; - @Mock private CodeRepository codeRepository; - @Spy private BCryptPasswordEncoder encoder; - @Mock private JavaMailSender javaMailSender; - @Mock private JwtUtil jwtUtil; - @Mock private CustomUserDetailsService customUserDetailService; - @Mock private RedisTemplate redisTemplate; + @Autowired ProfileRepository profileRepository; + @Autowired CodeRepository codeRepository; + @MockBean JavaMailSender javaMailSender; + + private String email; + private String purpose; private ProfileDto.Signup profileDto; - private Code code; + private MimeMessage mimeMessage; /* 테스트 별 초기 설정 */ @BeforeEach void conditionalSetUp(TestInfo testInfo) { - if ( - testInfo.getDisplayName().startsWith("회원가입 성공") || - testInfo.getDisplayName().startsWith("회원가입 실패") - ) + if (testInfo.getDisplayName().startsWith("회원가입")) { + email = "test1234@korea.ac.kr"; + purpose = "SIGN_UP"; profileDto = ProfileDto.Signup.builder() .email("test1234@korea.ac.kr") .password("test1234") @@ -69,210 +58,356 @@ void conditionalSetUp(TestInfo testInfo) .degree("MASTER") .semester(1) .department("test") - .code("12345678") .build(); + mimeMessage = mock(MimeMessage.class); - code = Code.builder() - .code_id(1L) - .email("test1234@korea.ac.kr") - .code("12345678") - .created_at(LocalDate.now()) - .build(); } } @Test - @DisplayName("회원가입 성공") - void signUp_success() + @DisplayName("회원가입 프로세스 성공") + void join_process_success() { // Given - given(codeRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.of(code)); - given(profileRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.empty()); - given(profileRepository.findByUsername(profileDto.getUsername())) - .willReturn(Optional.empty()); + given(javaMailSender.createMimeMessage()) + .willReturn(mimeMessage); // When + loginService.sendAuthenticationNumber(email, purpose); + + String code = codeRepository.findByEmail(email).get().getCode(); + loginService.checkAuthenticationNumber(email, code); + + profileDto.setCode(code); loginService.signUp(profileDto); // Then - verify(profileRepository, times(1)).save(any(Profile.class)); + verify(javaMailSender, times(1)).send(mimeMessage); + assertThat(profileRepository.findByEmail(email).isPresent()).isTrue(); } @Test - @DisplayName("회원가입 실패 1: 해당 이메일로 발송된 인증번호가 존재하지 않음") - void signUp_fail_1() + @DisplayName("회원가입 프로세스 실패 1 : 이메일 인증번호 전송 시, 고려대 이메일이 아닌 경우") + void join_process_failure_1() { // Given - given(codeRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.empty()); + email = "test1234@naver.com"; // When & Then - AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); - assertEquals(ErrorCode.CODE_NOT_FOUND, exception.getErrorCode()); - - verify(profileRepository, times(0)).save(any(Profile.class)); + AppException exception = assertThrows( + AppException.class, + () -> loginService.sendAuthenticationNumber(email, purpose) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.EMAIL_NOT_KOREA); } @Test - @DisplayName("회원가입 실패 2: 인증번호가 틀림") - void signUp_fail_2() + @DisplayName("회원가입 프로세스 실패 2 : purpose 형식이 잘못된 경우") + void join_process_failure_2() { // Given - profileDto.setCode("wrong_code"); - - given(codeRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.of(code)); + purpose = "wrong_purpose"; // When & Then - AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); - assertEquals(ErrorCode.WRONG_CODE, exception.getErrorCode()); - - verify(profileRepository, times(0)).save(any(Profile.class)); + AppException exception = assertThrows( + AppException.class, + () -> loginService.sendAuthenticationNumber(email, purpose) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.INVALID_PURPOSE); } @Test - @DisplayName("회원가입 실패 3: 이메일 중복") - void signUp_fail_3() + @DisplayName("회원가입 프로세스 실패 3 : 이메일 인증번호 확인 시, 고려대 이메일이 아닌 경우") + void join_process_failure_3() { // Given - given(codeRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.of(code)); - given(profileRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.of(new Profile())); + email = "test1234@naver.com"; + String code = "12345678"; // When & Then - AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); - assertEquals(ErrorCode.EMAIL_DUPLICATED, exception.getErrorCode()); + AppException exception = assertThrows( + AppException.class, + () -> loginService.checkAuthenticationNumber(email, code) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.EMAIL_NOT_KOREA); + } + + @Test + @DisplayName("회원가입 프로세스 실패 4 : 이메일 인증번호 확인 시, 해당 이메일로 발송된 인증번호가 없는 경우") + void join_process_failure_4() + { + // Given + String code = "12345678"; - verify(profileRepository, times(0)).save(any(Profile.class)); + // When & Then + AppException exception = assertThrows( + AppException.class, + () -> loginService.checkAuthenticationNumber(email, code) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.CODE_NOT_FOUND); } @Test - @DisplayName("회원가입 실패 4: 학번 형식 오류") - void signUp_fail_4() + @DisplayName("회원가입 프로세스 실패 5 : 이메일 인증번호 확인 시, 입력한 인증번호가 틀린 경우") + void join_process_failure_5() { // Given - profileDto.setStudent_id("12345678"); + String code = "12345678"; - given(codeRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.of(code)); - given(profileRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.empty()); + given(javaMailSender.createMimeMessage()) + .willReturn(mimeMessage); // When & Then - AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); - assertEquals(ErrorCode.INVALID_STUDENT_ID, exception.getErrorCode()); - - verify(profileRepository, times(0)).save(any(Profile.class)); + loginService.sendAuthenticationNumber(email, purpose); + AppException exception = assertThrows( + AppException.class, + () -> loginService.checkAuthenticationNumber(email, code) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.WRONG_CODE); } - @ParameterizedTest(name = "회원가입 실패 5: 비밀번호가 {0}") - @CsvSource({ - "test", - "test012345678901234567890123456789", - "0123456789", - "abcdefghij" - }) - @DisplayName("회원가입 실패 5: 비밀번호 형식 오류") - void signUp_fail_5(String password) + @Test + @DisplayName("회원가입 프로세스 실패 6 : 이메일 인증번호 확인 시, 이메일이 중복된 경우") + void join_process_failure_6() { // Given - profileDto.setPassword(password); - - given(codeRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.of(code)); - given(profileRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.empty()); + given(javaMailSender.createMimeMessage()) + .willReturn(mimeMessage); // When & Then - AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); - assertEquals(ErrorCode.INVALID_PASSWORD, exception.getErrorCode()); + loginService.sendAuthenticationNumber(email, purpose); + + Profile profile = Profile.builder() + .email("test1234@korea.ac.kr") + .password("test1234") + .username("test1234") + .student_id("1234567") + .degree("MASTER") + .semester(1) + .department("test") + .point(0) + .access_expiration_date(LocalDate.now()) + .created_at(LocalDate.now()) + .role("ROLE_USER") + .build(); + profileRepository.save(profile); + + String code = codeRepository.findByEmail(email).get().getCode(); + + AppException exception = assertThrows( + AppException.class, + () -> loginService.checkAuthenticationNumber(email, code) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.EMAIL_DUPLICATED); + } - verify(profileRepository, times(0)).save(any(Profile.class)); + @Test + @DisplayName("회원가입 프로세스 실패 7 : 회원가입 시, 해당 이메일로 발송된 인증번호가 없는 경우") + void join_process_failure_7() + { + // When & Then + AppException exception = assertThrows( + AppException.class, + () -> loginService.signUp(profileDto) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.CODE_NOT_FOUND); } @Test - @DisplayName("회원가입 실패 6: 닉네임 형식 오류") - void signUp_fail_6() + @DisplayName("회원가입 프로세스 실패 8 : 회원가입 시, 입력한 인증번호가 틀린 경우") + void join_process_failure_8() { // Given - profileDto.setUsername("01234567890123456789"); - - given(codeRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.of(code)); - given(profileRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.empty()); + given(javaMailSender.createMimeMessage()) + .willReturn(mimeMessage); // When & Then - AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); - assertEquals(ErrorCode.INVALID_USERNAME, exception.getErrorCode()); + loginService.sendAuthenticationNumber(email, purpose); + + String code = codeRepository.findByEmail(email).get().getCode(); + loginService.checkAuthenticationNumber(email, code); - verify(profileRepository, times(0)).save(any(Profile.class)); + profileDto.setCode("wrong_code"); + AppException exception = assertThrows( + AppException.class, + () -> loginService.signUp(profileDto) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.WRONG_CODE); } @Test - @DisplayName("회원가입 실패 7: 닉네임 중복") - void signUp_fail_7() + @DisplayName("회원가입 프로세스 실패 9 : 회원가입 시, 이메일이 중복된 경우") + void join_process_failure_9() { // Given - given(codeRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.of(code)); - given(profileRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.empty()); - given(profileRepository.findByUsername(profileDto.getUsername())) - .willReturn(Optional.of(new Profile())); + given(javaMailSender.createMimeMessage()) + .willReturn(mimeMessage); // When & Then - AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); - assertEquals(ErrorCode.USERNAME_DUPLICATED, exception.getErrorCode()); - - verify(profileRepository, times(0)).save(any(Profile.class)); + loginService.sendAuthenticationNumber(email, purpose); + + String code = codeRepository.findByEmail(email).get().getCode(); + loginService.checkAuthenticationNumber(email, code); + + Profile profile = Profile.builder() + .email("test1234@korea.ac.kr") + .password("test1234") + .username("test1234") + .student_id("1234567") + .degree("MASTER") + .semester(1) + .department("test") + .point(0) + .access_expiration_date(LocalDate.now()) + .created_at(LocalDate.now()) + .role("ROLE_USER") + .build(); + profileRepository.save(profile); + + profileDto.setCode(code); + AppException exception = assertThrows( + AppException.class, + () -> loginService.signUp(profileDto) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.EMAIL_DUPLICATED); } @Test - @DisplayName("회원가입 실패 8: 학위 형식 오류") - void signUp_fail_8() + @DisplayName("회원가입 프로세스 실패 10 : 회원가입 시, 학번 형식 오류") + void join_process_failure_10() { // Given - profileDto.setDegree("wrong_degree"); + profileDto.setStudent_id("1234567890"); - given(codeRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.of(code)); - given(profileRepository.findByEmail(profileDto.getEmail())) - .willReturn(Optional.empty()); - given(profileRepository.findByUsername(profileDto.getUsername())) - .willReturn(Optional.empty()); + given(javaMailSender.createMimeMessage()) + .willReturn(mimeMessage); // When & Then - AppException exception = assertThrows(AppException.class, () -> loginService.signUp(profileDto)); - assertEquals(ErrorCode.INVALID_DEGREE, exception.getErrorCode()); + loginService.sendAuthenticationNumber(email, purpose); - verify(profileRepository, times(0)).save(any(Profile.class)); - } + String code = codeRepository.findByEmail(email).get().getCode(); + loginService.checkAuthenticationNumber(email, code); - @Test - void sendAuthenticationNumber() { + profileDto.setCode(code); + AppException exception = assertThrows( + AppException.class, + () -> loginService.signUp(profileDto) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.INVALID_STUDENT_ID); } - @Test - void checkAuthenticationNumber() { - } + @ParameterizedTest(name = "회원가입 시, 비밀번호 형식 오류 {index} : password = {0}") + @CsvSource({ + "test1", + "test123456789012345678901234567890", + "testtesttest", + "12345678901234567890" + }) + @DisplayName("회원가입 프로세스 실패 11 : 회원가입 시, 비밀번호 형식 오류") + void join_process_failure_11(String password) + { + // Given + profileDto.setPassword(password); - @Test - void checkUsername() { + given(javaMailSender.createMimeMessage()) + .willReturn(mimeMessage); + + // When & Then + loginService.sendAuthenticationNumber(email, purpose); + + String code = codeRepository.findByEmail(email).get().getCode(); + loginService.checkAuthenticationNumber(email, code); + + profileDto.setCode(code); + AppException exception = assertThrows( + AppException.class, + () -> loginService.signUp(profileDto) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.INVALID_PASSWORD); } @Test - void resetPassword() { + @DisplayName("회원가입 프로세스 실패 12 : 회원가입 시, 닉네임 형식 오류") + void join_process_failure_12() + { + // Given + profileDto.setUsername("12345678901234567890"); + + given(javaMailSender.createMimeMessage()) + .willReturn(mimeMessage); + + // When & Then + loginService.sendAuthenticationNumber(email, purpose); + + String code = codeRepository.findByEmail(email).get().getCode(); + loginService.checkAuthenticationNumber(email, code); + + profileDto.setCode(code); + AppException exception = assertThrows( + AppException.class, + () -> loginService.signUp(profileDto) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.INVALID_USERNAME); } @Test - void checkLogin() { + @DisplayName("회원가입 프로세스 실패 13 : 회원가입 시, 닉네임이 중복된 경우") + void join_process_failure_13() + { + // Given + Profile profile = Profile.builder() + .email("another@korea.ac.kr") + .password("test1234") + .username("test1234") + .student_id("1234567") + .degree("MASTER") + .semester(1) + .department("test") + .point(0) + .access_expiration_date(LocalDate.now()) + .created_at(LocalDate.now()) + .role("ROLE_USER") + .build(); + profileRepository.save(profile); + + given(javaMailSender.createMimeMessage()) + .willReturn(mimeMessage); + + // When & Then + loginService.sendAuthenticationNumber(email, purpose); + + String code = codeRepository.findByEmail(email).get().getCode(); + loginService.checkAuthenticationNumber(email, code); + + profileDto.setCode(code); + AppException exception = assertThrows( + AppException.class, + () -> loginService.signUp(profileDto) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.USERNAME_DUPLICATED); } @Test - void refreshToken() { + @DisplayName("회원가입 프로세스 실패 14 : 회원가입 시, 학위 형식 오류") + void join_process_failure_14() + { + // Given + profileDto.setDegree("wrong_degree"); + + given(javaMailSender.createMimeMessage()) + .willReturn(mimeMessage); + + // When & Then + loginService.sendAuthenticationNumber(email, purpose); + + String code = codeRepository.findByEmail(email).get().getCode(); + loginService.checkAuthenticationNumber(email, code); + + profileDto.setCode(code); + AppException exception = assertThrows( + AppException.class, + () -> loginService.signUp(profileDto) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.INVALID_DEGREE); } } \ No newline at end of file From d10339241daf388dc39a48fcf9ac5a94470caa83 Mon Sep 17 00:00:00 2001 From: kimyerak Date: Sun, 2 Feb 2025 19:14:07 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20privacy,=20timetable=2013?= =?UTF-8?q?=EA=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PrivacyController.java | 29 +++++----- .../controller/TimetableController.java | 34 +++++------ .../dto/CourseAssignmentDto.java | 13 +++++ .../example/Devkor_project/dto/CourseDto.java | 8 +++ .../dto/PrivacyAssignmentDto.java | 17 ++++++ .../Devkor_project/dto/PrivacyDto.java | 12 ++++ .../Devkor_project/dto/PrivacyUpdateDto.java | 26 +++++++++ .../Devkor_project/dto/TimetableDto.java | 6 +- .../dto/TimetableWithDetailsDto.java | 4 +- .../Devkor_project/exception/ErrorCode.java | 6 +- .../service/PrivacyService.java | 35 +++++++---- .../service/TimetableService.java | 58 ++++++++++++------- 12 files changed, 178 insertions(+), 70 deletions(-) create mode 100644 src/main/java/com/example/Devkor_project/dto/CourseAssignmentDto.java create mode 100644 src/main/java/com/example/Devkor_project/dto/PrivacyAssignmentDto.java create mode 100644 src/main/java/com/example/Devkor_project/dto/PrivacyUpdateDto.java diff --git a/src/main/java/com/example/Devkor_project/controller/PrivacyController.java b/src/main/java/com/example/Devkor_project/controller/PrivacyController.java index 4b05345..942fed2 100644 --- a/src/main/java/com/example/Devkor_project/controller/PrivacyController.java +++ b/src/main/java/com/example/Devkor_project/controller/PrivacyController.java @@ -1,6 +1,7 @@ package com.example.Devkor_project.controller; import com.example.Devkor_project.dto.PrivacyDto; +import com.example.Devkor_project.dto.PrivacyUpdateDto; import com.example.Devkor_project.dto.ResponseDto; import com.example.Devkor_project.service.PrivacyService; import com.example.Devkor_project.configuration.VersionProvider; @@ -68,8 +69,8 @@ public ResponseEntity createPrivacy(@Valid @RequestBody Pri @Operation(summary = "개인일정 수정") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") @Parameter(in = ParameterIn.PATH, name = "privacyId", description = "수정할 개인일정 ID") - public PrivacyDto updatePrivacy(@PathVariable Long privacyId, @Valid @RequestBody PrivacyDto privacyDto, Principal principal) { - return privacyService.updatePrivacy(privacyId, privacyDto, principal); + public ResponseEntity updatePrivacy(@PathVariable Long privacyId, @Valid @RequestBody PrivacyUpdateDto privacyUpdateDto, Principal principal) { + return privacyService.updatePrivacy(privacyId, privacyUpdateDto, principal); } @DeleteMapping("/{privacyId}") @@ -79,17 +80,17 @@ public void deletePrivacy(@PathVariable Long privacyId, Principal principal) { privacyService.deletePrivacy(privacyId, principal); } - @PostMapping("/{timetableId}/privacy") - @Operation(summary = "특정 시간표에 개인일정 추가") - @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") - public PrivacyDto addPrivacyToTimetable(@PathVariable Long timetableId, @Valid @RequestBody PrivacyDto privacyDto, Principal principal) { - return privacyService.addPrivacyToTimetable(timetableId, privacyDto, principal); - } +// @PostMapping("/{timetableId}/privacy") +// @Operation(summary = "특정 시간표에 개인일정 추가(아래 timetable쪽에 똑같은 api있어서 삭제)") +// @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") +// public PrivacyDto addPrivacyToTimetable(@PathVariable Long timetableId, @Valid @RequestBody PrivacyDto privacyDto, Principal principal) { +// return privacyService.addPrivacyToTimetable(timetableId, privacyDto, principal); +// } - @DeleteMapping("/{timetableId}/privacy/{privacyId}") - @Operation(summary = "특정 시간표에서 개인일정 삭제") - @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") - public void removePrivacyFromTimetable(@PathVariable Long timetableId, @PathVariable Long privacyId, Principal principal) { - privacyService.removePrivacyFromTimetable(timetableId, privacyId, principal); - } +// @DeleteMapping("/{timetableId}/privacy/{privacyId}") +// @Operation(summary = "특정 시간표에서 개인일정 삭제(아래 timetable쪽에 똑같은 api있어서 삭제)") +// @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") +// public void removePrivacyFromTimetable(@PathVariable Long timetableId, @PathVariable Long privacyId, Principal principal) { +// privacyService.removePrivacyFromTimetable(timetableId, privacyId, principal); +// } } diff --git a/src/main/java/com/example/Devkor_project/controller/TimetableController.java b/src/main/java/com/example/Devkor_project/controller/TimetableController.java index c3e2ea0..30cc49a 100644 --- a/src/main/java/com/example/Devkor_project/controller/TimetableController.java +++ b/src/main/java/com/example/Devkor_project/controller/TimetableController.java @@ -1,8 +1,6 @@ package com.example.Devkor_project.controller; -import com.example.Devkor_project.dto.PrivacyDto; -import com.example.Devkor_project.dto.TimetableDto; -import com.example.Devkor_project.dto.ResponseDto; +import com.example.Devkor_project.dto.*; import com.example.Devkor_project.entity.Timetable; import com.example.Devkor_project.service.TimetableService; import com.example.Devkor_project.configuration.VersionProvider; @@ -49,8 +47,8 @@ public ResponseEntity createTimetable( @PostMapping("/{timetableId}/course") @Operation(summary = "시간표에 강의 추가") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") - public ResponseEntity addCourseToTimetable(@PathVariable Long timetableId, @RequestParam Long courseId, Principal principal) { - return timetableService.addCourseToTimetable(timetableId, courseId, principal); + public ResponseEntity addCourseToTimetable(@PathVariable Long timetableId, @Valid @RequestBody CourseAssignmentDto requestDto, Principal principal) { + return timetableService.addCourseToTimetable(timetableId, requestDto, principal); } /** 🟢 강의 제거 */ @@ -63,24 +61,24 @@ public ResponseEntity removeCourseFromTimetable(@PathVariable Long timetableI /** 🟢 개인 일정 추가 */ @PostMapping("/{timetableId}/privacy") - @Operation(summary = "시간표에 개인 일정 추가") + @Operation(summary = "시간표에 개인 일정 추가(이렇게 추후에 추가해도 되고, privacy 애초에 POST할때 시간표 id지정해서 넣어주는 것도 가능") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") - public ResponseEntity addPrivacyToTimetable(@PathVariable Long timetableId, @Valid @RequestBody PrivacyDto privacyDto, Principal principal) { - return timetableService.addPrivacyToTimetable(timetableId, privacyDto, principal); + public ResponseEntity addPrivacyToTimetable(@PathVariable Long timetableId, @Valid @RequestBody PrivacyAssignmentDto requestDto, Principal principal) { + return timetableService.addPrivacyToTimetable(timetableId, requestDto, principal); } - /** 🟢 로그인된 사용자의 모든 시간표 조회 */ - @GetMapping - @Operation(summary = "로그인된 사용자의 모든 시간표 조회") - @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") - - public ResponseEntity getAllTimetablesForUser(Principal principal) { - return timetableService.getAllTimetablesForUser(principal); - } +// /** 🟢 로그인된 사용자의 모든 시간표 조회 */ +// @GetMapping +// @Operation(summary = "로그인된 사용자의 정보와 시간표들 조회(너무 복잡. 잘안쓰일듯)") +// @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") +// +// public ResponseEntity getAllTimetablesForUser(Principal principal) { +// return timetableService.getAllTimetablesForUser(principal); +// } /** 🟢 로그인된 사용자의 시간표 이름과 ID 조회 */ @GetMapping("/names-and-ids") - @Operation(summary = "로그인된 사용자의 시간표 이름과 ID 조회") + @Operation(summary = "로그인된 사용자의 시간표 이름과 ID 조회(이게 목록 불러오기임)") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") public ResponseEntity getTimetableNamesAndIds(Principal principal) { return timetableService.getTimetableNamesAndIds(principal); @@ -91,7 +89,7 @@ public ResponseEntity getTimetableNamesAndIds(Principal pri @Operation(summary = "특정 시간표 조회") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") public ResponseEntity getTimetableById(@PathVariable Long timetableId, Principal principal) { - return timetableService.getTimetableById(timetableId, principal); + return timetableService.getTimetableByIdWithDetails(timetableId, principal); } /** 🟢 시간표에서 개인 일정 제거 */ diff --git a/src/main/java/com/example/Devkor_project/dto/CourseAssignmentDto.java b/src/main/java/com/example/Devkor_project/dto/CourseAssignmentDto.java new file mode 100644 index 0000000..720c2d5 --- /dev/null +++ b/src/main/java/com/example/Devkor_project/dto/CourseAssignmentDto.java @@ -0,0 +1,13 @@ +package com.example.Devkor_project.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class CourseAssignmentDto { + + @NotNull(message = "courseId는 null일 수 없습니다.") + @Schema(description = "추가할 강의 ID") + private Long courseId; +} diff --git a/src/main/java/com/example/Devkor_project/dto/CourseDto.java b/src/main/java/com/example/Devkor_project/dto/CourseDto.java index 3d8198a..fb28454 100644 --- a/src/main/java/com/example/Devkor_project/dto/CourseDto.java +++ b/src/main/java/com/example/Devkor_project/dto/CourseDto.java @@ -492,4 +492,12 @@ public static CourseDto.ExpiredDetail entityToExpiredDetail( .isBookmark(isBookmark) .build(); } + public static CourseDto.Basic fromCourse(Course course) { + return CourseDto.Basic.builder() + .course_id(course.getCourse_id()) + .course_code(course.getCourse_code()) + .name(course.getName()) + .professor(course.getProfessor()) + .build(); + } } diff --git a/src/main/java/com/example/Devkor_project/dto/PrivacyAssignmentDto.java b/src/main/java/com/example/Devkor_project/dto/PrivacyAssignmentDto.java new file mode 100644 index 0000000..a3d8491 --- /dev/null +++ b/src/main/java/com/example/Devkor_project/dto/PrivacyAssignmentDto.java @@ -0,0 +1,17 @@ +package com.example.Devkor_project.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Builder +@Schema(description = "시간표에 개인 일정을 추가하기 위한 요청 DTO") +public class PrivacyAssignmentDto { + + @NotNull(message = "[privacyId] cannot be null.") + @Schema(description = "추가할 개인 일정 ID") + private Long privacyId; +} diff --git a/src/main/java/com/example/Devkor_project/dto/PrivacyDto.java b/src/main/java/com/example/Devkor_project/dto/PrivacyDto.java index c96d017..3d90213 100644 --- a/src/main/java/com/example/Devkor_project/dto/PrivacyDto.java +++ b/src/main/java/com/example/Devkor_project/dto/PrivacyDto.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import com.example.Devkor_project.entity.Privacy; import lombok.Builder; import lombok.Getter; @@ -46,4 +47,15 @@ public class PrivacyDto { @Schema(description = "수정 시간") private LocalDateTime updatedAt; + + public static PrivacyDto fromPrivacy(Privacy privacy) { + return PrivacyDto.builder() + .id(privacy.getId()) + .name(privacy.getName()) + .day(privacy.getDay()) + .startTime(privacy.getStartTime()) + .finishTime(privacy.getFinishTime()) + .location(privacy.getLocation()) + .build(); + } } diff --git a/src/main/java/com/example/Devkor_project/dto/PrivacyUpdateDto.java b/src/main/java/com/example/Devkor_project/dto/PrivacyUpdateDto.java new file mode 100644 index 0000000..3d80f1c --- /dev/null +++ b/src/main/java/com/example/Devkor_project/dto/PrivacyUpdateDto.java @@ -0,0 +1,26 @@ +package com.example.Devkor_project.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalTime; + +@Getter +@NoArgsConstructor +public class PrivacyUpdateDto { + @NotBlank(message = "이름은 필수 입력 항목입니다.") + private String name; + + @NotBlank(message = "요일은 필수 입력 항목입니다.") + private String day; + + @NotNull(message = "시작 시간은 필수 입력 항목입니다.") + private LocalTime startTime; + + @NotNull(message = "종료 시간은 필수 입력 항목입니다.") + private LocalTime finishTime; + + private String location; +} diff --git a/src/main/java/com/example/Devkor_project/dto/TimetableDto.java b/src/main/java/com/example/Devkor_project/dto/TimetableDto.java index 7979b88..65ad0f2 100644 --- a/src/main/java/com/example/Devkor_project/dto/TimetableDto.java +++ b/src/main/java/com/example/Devkor_project/dto/TimetableDto.java @@ -14,9 +14,9 @@ @Builder @Schema(description = "응답 성공 DTO") public class TimetableDto { - @NotBlank(message = "[profileId] cannot be blank.") - @Schema(description = "profile ID") - private Long profileId; +// @NotBlank(message = "[profileId] cannot be blank.") +// @Schema(description = "profile ID") +// private Long profileId; @Schema(description = "Timetable ID") private Long id; diff --git a/src/main/java/com/example/Devkor_project/dto/TimetableWithDetailsDto.java b/src/main/java/com/example/Devkor_project/dto/TimetableWithDetailsDto.java index 571bcae..ce4b026 100644 --- a/src/main/java/com/example/Devkor_project/dto/TimetableWithDetailsDto.java +++ b/src/main/java/com/example/Devkor_project/dto/TimetableWithDetailsDto.java @@ -19,6 +19,8 @@ public class TimetableWithDetailsDto { private Long id; private Long profileId; private String name; - private List courses; // 연관된 Course 정보 + private List courses; // 연관된 Course 정보 private List privacies; // 연관된 Privacy 정보 + + } diff --git a/src/main/java/com/example/Devkor_project/exception/ErrorCode.java b/src/main/java/com/example/Devkor_project/exception/ErrorCode.java index ab10c89..b9c0c87 100644 --- a/src/main/java/com/example/Devkor_project/exception/ErrorCode.java +++ b/src/main/java/com/example/Devkor_project/exception/ErrorCode.java @@ -44,7 +44,11 @@ public enum ErrorCode INVALID_PERIOD(HttpStatus.BAD_REQUEST, "해당 교시는 유효하지 않습니다."), INVALID_TIME_LOCATION(HttpStatus.BAD_REQUEST, "해당 시간 및 장소 정보는 유효하지 않습니다."), TIMETABLE_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 시간표를 찾을 수 없습니다."), - PRIVACY_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 개인 일정을 찾을 수 없습니다."); + PRIVACY_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 개인 일정을 찾을 수 없습니다."), + DUPLICATE_ENTRY(HttpStatus.BAD_REQUEST, "이미 추가된 일정입니다."), + SCHEDULE_CONFLICT(HttpStatus.CONFLICT, "다른 일정과 시간이 겹칩니다."); + + private final HttpStatus httpStatus; private final String message; } diff --git a/src/main/java/com/example/Devkor_project/service/PrivacyService.java b/src/main/java/com/example/Devkor_project/service/PrivacyService.java index 55f3c23..729d6cc 100644 --- a/src/main/java/com/example/Devkor_project/service/PrivacyService.java +++ b/src/main/java/com/example/Devkor_project/service/PrivacyService.java @@ -1,12 +1,18 @@ package com.example.Devkor_project.service; import com.example.Devkor_project.dto.PrivacyDto; +import com.example.Devkor_project.dto.PrivacyUpdateDto; +import com.example.Devkor_project.dto.ResponseDto; import com.example.Devkor_project.entity.Privacy; import com.example.Devkor_project.entity.Timetable; +import com.example.Devkor_project.exception.AppException; +import com.example.Devkor_project.exception.ErrorCode; import com.example.Devkor_project.repository.PrivacyRepository; import com.example.Devkor_project.repository.TimetableRepository; import lombok.RequiredArgsConstructor; import jakarta.transaction.Transactional; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import java.security.Principal; @@ -63,25 +69,32 @@ public PrivacyDto createPrivacy(PrivacyDto privacyDto, Principal principal) { // 개인일정 수정 @Transactional - public PrivacyDto updatePrivacy(Long id, PrivacyDto privacyDto, Principal principal) { - Privacy privacy = privacyRepository.findById(id) - .orElseThrow(() -> new RuntimeException("Privacy not found")); + public ResponseEntity updatePrivacy(Long privacyId, PrivacyUpdateDto updateDto, Principal principal) { + Privacy privacy = privacyRepository.findById(privacyId) + .orElseThrow(() -> new AppException(ErrorCode.PRIVACY_NOT_FOUND, "해당 개인 일정을 찾을 수 없습니다.")); validateTimetablesOwnership(privacy.getTimetables().stream() .map(Timetable::getId) .collect(Collectors.toList()), principal); - List timetables = timetableRepository.findAllById(privacyDto.getTimetableIds()); - privacy.setName(privacyDto.getName()); - privacy.setDay(privacyDto.getDay()); - privacy.setStartTime(privacyDto.getStartTime()); - privacy.setFinishTime(privacyDto.getFinishTime()); - privacy.setLocation(privacyDto.getLocation()); - privacy.setTimetables(timetables); + // 기존 일정 정보 업데이트 + privacy.setName(updateDto.getName()); + privacy.setDay(updateDto.getDay()); + privacy.setStartTime(updateDto.getStartTime()); + privacy.setFinishTime(updateDto.getFinishTime()); + privacy.setLocation(updateDto.getLocation()); - return convertToDto(privacyRepository.save(privacy)); + privacyRepository.save(privacy); + + return ResponseEntity.status(HttpStatus.OK) + .body(ResponseDto.Success.builder() + .message("개인 일정이 성공적으로 수정되었습니다.") + .data(convertToDto(privacy)) + .version("v1.1.4") + .build()); } + // 개인일정 삭제 @Transactional public void deletePrivacy(Long id, Principal principal) { diff --git a/src/main/java/com/example/Devkor_project/service/TimetableService.java b/src/main/java/com/example/Devkor_project/service/TimetableService.java index 78d26d2..e3f76be 100644 --- a/src/main/java/com/example/Devkor_project/service/TimetableService.java +++ b/src/main/java/com/example/Devkor_project/service/TimetableService.java @@ -1,8 +1,6 @@ package com.example.Devkor_project.service; -import com.example.Devkor_project.dto.PrivacyDto; -import com.example.Devkor_project.dto.ResponseDto; -import com.example.Devkor_project.dto.TimetableDto; +import com.example.Devkor_project.dto.*; import com.example.Devkor_project.entity.Course; import com.example.Devkor_project.entity.Privacy; import com.example.Devkor_project.entity.Profile; @@ -73,7 +71,7 @@ public ResponseEntity getTimetableNamesAndIds(Principal pri Profile profile = getProfileByPrincipal(principal); List timetableDtos = timetableRepository.findByProfile_profileId(profile.getProfile_id()).stream() - .map(t -> new TimetableDto(null, t.getId(), t.getName())) + .map(t -> new TimetableDto(t.getId(), t.getName())) .collect(Collectors.toList()); return ResponseEntity.status(HttpStatus.OK) @@ -85,27 +83,41 @@ public ResponseEntity getTimetableNamesAndIds(Principal pri } /** - * 특정 시간표 조회 + * 특정 시간표 조회 (상세 정보 포함) */ @Transactional - public ResponseEntity getTimetableById(Long timetableId, Principal principal) { + public ResponseEntity getTimetableByIdWithDetails(Long timetableId, Principal principal) { Timetable timetable = validateProfileOwnership(timetableId, principal); + // DTO 변환 + TimetableWithDetailsDto dto = TimetableWithDetailsDto.builder() + .id(timetable.getId()) + .profileId(timetable.getProfile().getProfile_id()) + .name(timetable.getName()) + .courses(timetable.getCourses().stream() + .map(CourseDto::fromCourse) // ✅ CourseDto 변환 메서드 사용 + .collect(Collectors.toList())) + .privacies(timetable.getPrivacies().stream() + .map(PrivacyDto::fromPrivacy) // ✅ PrivacyDto 변환 메서드 사용 + .collect(Collectors.toList())) + .build(); + return ResponseEntity.status(HttpStatus.OK) .body(ResponseDto.Success.builder() .message("특정 시간표를 성공적으로 조회하였습니다.") - .data(timetable) + .data(dto) .version("v1.1.4") .build()); } + /** * 시간표에 강의 추가 */ @Transactional - public ResponseEntity addCourseToTimetable(Long timetableId, Long courseId, Principal principal) { + public ResponseEntity addCourseToTimetable(Long timetableId, CourseAssignmentDto requestDto, Principal principal) { Timetable timetable = validateProfileOwnership(timetableId, principal); - Course course = courseRepository.findById(courseId) + Course course = courseRepository.findById(requestDto.getCourseId()) .orElseThrow(() -> new AppException(ErrorCode.COURSE_NOT_FOUND, "해당 강의를 찾을 수 없습니다.")); timetable.getCourses().add(course); @@ -143,32 +155,34 @@ public ResponseEntity removeCourseFromTimetable(Long timeta .build()); } - /** - * 시간표에 개인 일정 추가 - */ + /** 🟢 시간표에 개인 일정 추가 (중복은 방지되도록 코드 수정 완) */ @Transactional - public ResponseEntity addPrivacyToTimetable(Long timetableId, PrivacyDto privacyDto, Principal principal) { + public ResponseEntity addPrivacyToTimetable( + Long timetableId, PrivacyAssignmentDto requestDto, Principal principal) { + Timetable timetable = validateProfileOwnership(timetableId, principal); + Privacy privacy = privacyRepository.findById(requestDto.getPrivacyId()) + .orElseThrow(() -> new AppException(ErrorCode.PRIVACY_NOT_FOUND, "해당 개인 일정을 찾을 수 없습니다.")); - Privacy privacy = Privacy.builder() - .name(privacyDto.getName()) - .day(privacyDto.getDay()) - .startTime(privacyDto.getStartTime()) - .finishTime(privacyDto.getFinishTime()) - .location(privacyDto.getLocation()) - .build(); + if (timetable.getPrivacies().contains(privacy)) { + throw new AppException(ErrorCode.DUPLICATE_ENTRY, "해당 개인 일정은 이미 추가되어 있습니다."); + } + // 시간표에 개인 일정 추가 + timetable.getPrivacies().add(privacy); privacy.getTimetables().add(timetable); + + timetableRepository.save(timetable); privacyRepository.save(privacy); return ResponseEntity.status(HttpStatus.OK) .body(ResponseDto.Success.builder() .message("개인 일정이 시간표에 추가되었습니다.") - .data(privacy) .version("v1.1.4") .build()); } + /** * 시간표에서 개인 일정 제거 */ @@ -202,7 +216,7 @@ public ResponseEntity updateTimetableName(Long timetableId, return ResponseEntity.status(HttpStatus.OK) .body(ResponseDto.Success.builder() .message("시간표 이름이 성공적으로 변경되었습니다.") - .data(timetable) + .data(new TimetableDto(timetable.getId(), timetable.getName())) .version("v1.1.4") .build()); } From 1e4b9db32e249b31b6d9b260c61ca0c2e3487f51 Mon Sep 17 00:00:00 2001 From: kimyerak Date: Sun, 2 Feb 2025 19:24:17 +0900 Subject: [PATCH 07/11] =?UTF-8?q?fix:=20=EA=B0=9C=EC=9D=B8=EC=9D=BC?= =?UTF-8?q?=EC=A0=95=EC=9D=80=20=EC=9D=B4=EB=A6=84,=20=EC=9E=A5=EC=86=8C?= =?UTF-8?q?=EB=A7=8C=20=EC=88=98=EC=A0=95=EA=B0=80=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PrivacyController.java | 2 +- .../Devkor_project/dto/PrivacyUpdateDto.java | 14 ++++++-------- .../Devkor_project/service/PrivacyService.java | 3 --- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/example/Devkor_project/controller/PrivacyController.java b/src/main/java/com/example/Devkor_project/controller/PrivacyController.java index 942fed2..526386f 100644 --- a/src/main/java/com/example/Devkor_project/controller/PrivacyController.java +++ b/src/main/java/com/example/Devkor_project/controller/PrivacyController.java @@ -66,7 +66,7 @@ public ResponseEntity createPrivacy(@Valid @RequestBody Pri } @PutMapping("/{privacyId}") - @Operation(summary = "개인일정 수정") + @Operation(summary = "개인일정 수정 (이름 & 장소만 변경 가능)") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") @Parameter(in = ParameterIn.PATH, name = "privacyId", description = "수정할 개인일정 ID") public ResponseEntity updatePrivacy(@PathVariable Long privacyId, @Valid @RequestBody PrivacyUpdateDto privacyUpdateDto, Principal principal) { diff --git a/src/main/java/com/example/Devkor_project/dto/PrivacyUpdateDto.java b/src/main/java/com/example/Devkor_project/dto/PrivacyUpdateDto.java index 3d80f1c..783c182 100644 --- a/src/main/java/com/example/Devkor_project/dto/PrivacyUpdateDto.java +++ b/src/main/java/com/example/Devkor_project/dto/PrivacyUpdateDto.java @@ -1,5 +1,6 @@ package com.example.Devkor_project.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Getter; @@ -10,17 +11,14 @@ @Getter @NoArgsConstructor public class PrivacyUpdateDto { + //개인일정은 이름, 장소만 바꿀 수 있음 + //요일.시간 바꾸는건 아예 새로운 일정으로 "생성"하는게 깔끔 + //요일.시간 바꾸면... 모든 시간표에서 겹치지 않게 double check 하느라 비효율적 @NotBlank(message = "이름은 필수 입력 항목입니다.") + @Schema(description = "개인 일정 이름") private String name; - @NotBlank(message = "요일은 필수 입력 항목입니다.") - private String day; - - @NotNull(message = "시작 시간은 필수 입력 항목입니다.") - private LocalTime startTime; - - @NotNull(message = "종료 시간은 필수 입력 항목입니다.") - private LocalTime finishTime; + @Schema(description = "장소") private String location; } diff --git a/src/main/java/com/example/Devkor_project/service/PrivacyService.java b/src/main/java/com/example/Devkor_project/service/PrivacyService.java index 729d6cc..dd4c6dc 100644 --- a/src/main/java/com/example/Devkor_project/service/PrivacyService.java +++ b/src/main/java/com/example/Devkor_project/service/PrivacyService.java @@ -79,9 +79,6 @@ public ResponseEntity updatePrivacy(Long privacyId, Privacy // 기존 일정 정보 업데이트 privacy.setName(updateDto.getName()); - privacy.setDay(updateDto.getDay()); - privacy.setStartTime(updateDto.getStartTime()); - privacy.setFinishTime(updateDto.getFinishTime()); privacy.setLocation(updateDto.getLocation()); privacyRepository.save(privacy); From 38cd8ba6bf999fdda109e0458a9a91720dfcc07c Mon Sep 17 00:00:00 2001 From: YunJaeHoon Date: Sun, 2 Feb 2025 20:48:25 +0900 Subject: [PATCH 08/11] =?UTF-8?q?FTR:=20LoginService=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Devkor_project/service/LoginService.java | 9 +- .../service/LoginServiceTest.java | 301 +++++++++++++----- 2 files changed, 224 insertions(+), 86 deletions(-) diff --git a/src/main/java/com/example/Devkor_project/service/LoginService.java b/src/main/java/com/example/Devkor_project/service/LoginService.java index c632937..073b3c6 100644 --- a/src/main/java/com/example/Devkor_project/service/LoginService.java +++ b/src/main/java/com/example/Devkor_project/service/LoginService.java @@ -95,10 +95,7 @@ public void signUp(ProfileDto.Signup dto) throw new AppException(ErrorCode.INVALID_USERNAME, dto.getUsername()); // 닉네임이 중복된 경우, 예외 처리 - profileRepository.findByUsername(dto.getUsername()) - .ifPresent(user -> { - throw new AppException(ErrorCode.USERNAME_DUPLICATED, dto.getUsername()); - }); + checkUsername(dto.getUsername()); // 학위가 'MASTER' 또는 'DEGREE'인지 체크 if(!Objects.equals(dto.getDegree(), "MASTER") && !Objects.equals(dto.getDegree(), "DOCTOR")) @@ -280,7 +277,7 @@ public void checkUsername(String username) @Transactional public void resetPassword(ProfileDto.ResetPassword dto) { - // 고려대 이메일인지 확인 + // 고려대 이메일이 아닌 경우 if (!dto.getEmail().endsWith("@korea.ac.kr")) throw new AppException(ErrorCode.EMAIL_NOT_KOREA, dto.getEmail()); @@ -368,7 +365,7 @@ public ProfileDto.CheckLogin checkLogin(Principal principal) .build(); } - /* access token 재발급 서비스 */ + /* Access token 재발급 서비스 */ @Transactional public String refreshToken(HttpServletRequest request) { diff --git a/src/test/java/com/example/Devkor_project/service/LoginServiceTest.java b/src/test/java/com/example/Devkor_project/service/LoginServiceTest.java index 264ac70..f0ca2fe 100644 --- a/src/test/java/com/example/Devkor_project/service/LoginServiceTest.java +++ b/src/test/java/com/example/Devkor_project/service/LoginServiceTest.java @@ -6,30 +6,43 @@ import com.example.Devkor_project.exception.ErrorCode; import com.example.Devkor_project.repository.CodeRepository; import com.example.Devkor_project.repository.ProfileRepository; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.mail.internet.MimeMessage; +import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.mockito.BDDMockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.transaction.annotation.Transactional; +import java.security.Principal; import java.time.LocalDate; +import java.util.Map; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest +@AutoConfigureMockMvc @Transactional class LoginServiceTest { + @Autowired MockMvc mockMvc; @Autowired(required = false) LoginService loginService; @Autowired ProfileRepository profileRepository; @@ -38,7 +51,6 @@ class LoginServiceTest @MockBean JavaMailSender javaMailSender; private String email; - private String purpose; private ProfileDto.Signup profileDto; private MimeMessage mimeMessage; @@ -46,44 +58,52 @@ class LoginServiceTest @BeforeEach void conditionalSetUp(TestInfo testInfo) { - if (testInfo.getDisplayName().startsWith("회원가입")) - { - email = "test1234@korea.ac.kr"; - purpose = "SIGN_UP"; - profileDto = ProfileDto.Signup.builder() - .email("test1234@korea.ac.kr") - .password("test1234") - .username("test1234") - .student_id("1234567") - .degree("MASTER") - .semester(1) - .department("test") - .build(); - mimeMessage = mock(MimeMessage.class); - - } + email = "test1234@korea.ac.kr"; + profileDto = ProfileDto.Signup.builder() + .email(email) + .password("test1234") + .username("test1234") + .student_id("1234567") + .degree("MASTER") + .semester(1) + .department("test") + .build(); + mimeMessage = mock(MimeMessage.class); } - @Test - @DisplayName("회원가입 프로세스 성공") - void join_process_success() + // profileDto에 해당하는 계정 생성 + void createAccount(String email) { - // Given given(javaMailSender.createMimeMessage()) .willReturn(mimeMessage); - // When - loginService.sendAuthenticationNumber(email, purpose); + loginService.sendAuthenticationNumber(email, "SIGN_UP"); String code = codeRepository.findByEmail(email).get().getCode(); loginService.checkAuthenticationNumber(email, code); + profileDto.setEmail(email); profileDto.setCode(code); loginService.signUp(profileDto); + } + + @Test + @DisplayName("회원가입 프로세스 성공") + void join_process_success() throws Exception + { + // Given + createAccount(email); // Then verify(javaMailSender, times(1)).send(mimeMessage); assertThat(profileRepository.findByEmail(email).isPresent()).isTrue(); + + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("email", profileDto.getEmail()) + .param("password", profileDto.getPassword()) + .param("remember-me", "true")) + .andExpect(status().isOk()); } @Test @@ -96,7 +116,7 @@ void join_process_failure_1() // When & Then AppException exception = assertThrows( AppException.class, - () -> loginService.sendAuthenticationNumber(email, purpose) + () -> loginService.sendAuthenticationNumber(email, "SIGN_UP") ); assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.EMAIL_NOT_KOREA); } @@ -105,13 +125,10 @@ void join_process_failure_1() @DisplayName("회원가입 프로세스 실패 2 : purpose 형식이 잘못된 경우") void join_process_failure_2() { - // Given - purpose = "wrong_purpose"; - // When & Then AppException exception = assertThrows( AppException.class, - () -> loginService.sendAuthenticationNumber(email, purpose) + () -> loginService.sendAuthenticationNumber(email, "wrong_purpose") ); assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.INVALID_PURPOSE); } @@ -158,7 +175,7 @@ void join_process_failure_5() .willReturn(mimeMessage); // When & Then - loginService.sendAuthenticationNumber(email, purpose); + loginService.sendAuthenticationNumber(email, "SIGN_UP"); AppException exception = assertThrows( AppException.class, () -> loginService.checkAuthenticationNumber(email, code) @@ -175,22 +192,9 @@ void join_process_failure_6() .willReturn(mimeMessage); // When & Then - loginService.sendAuthenticationNumber(email, purpose); + loginService.sendAuthenticationNumber(email, "SIGN_UP"); - Profile profile = Profile.builder() - .email("test1234@korea.ac.kr") - .password("test1234") - .username("test1234") - .student_id("1234567") - .degree("MASTER") - .semester(1) - .department("test") - .point(0) - .access_expiration_date(LocalDate.now()) - .created_at(LocalDate.now()) - .role("ROLE_USER") - .build(); - profileRepository.save(profile); + createAccount(email); String code = codeRepository.findByEmail(email).get().getCode(); @@ -222,7 +226,7 @@ void join_process_failure_8() .willReturn(mimeMessage); // When & Then - loginService.sendAuthenticationNumber(email, purpose); + loginService.sendAuthenticationNumber(email, "SIGN_UP"); String code = codeRepository.findByEmail(email).get().getCode(); loginService.checkAuthenticationNumber(email, code); @@ -244,25 +248,13 @@ void join_process_failure_9() .willReturn(mimeMessage); // When & Then - loginService.sendAuthenticationNumber(email, purpose); + loginService.sendAuthenticationNumber(email, "SIGN_UP"); String code = codeRepository.findByEmail(email).get().getCode(); loginService.checkAuthenticationNumber(email, code); - Profile profile = Profile.builder() - .email("test1234@korea.ac.kr") - .password("test1234") - .username("test1234") - .student_id("1234567") - .degree("MASTER") - .semester(1) - .department("test") - .point(0) - .access_expiration_date(LocalDate.now()) - .created_at(LocalDate.now()) - .role("ROLE_USER") - .build(); - profileRepository.save(profile); + createAccount(email); + code = codeRepository.findByEmail(email).get().getCode(); profileDto.setCode(code); AppException exception = assertThrows( @@ -283,7 +275,7 @@ void join_process_failure_10() .willReturn(mimeMessage); // When & Then - loginService.sendAuthenticationNumber(email, purpose); + loginService.sendAuthenticationNumber(email, "SIGN_UP"); String code = codeRepository.findByEmail(email).get().getCode(); loginService.checkAuthenticationNumber(email, code); @@ -313,7 +305,7 @@ void join_process_failure_11(String password) .willReturn(mimeMessage); // When & Then - loginService.sendAuthenticationNumber(email, purpose); + loginService.sendAuthenticationNumber(email, "SIGN_UP"); String code = codeRepository.findByEmail(email).get().getCode(); loginService.checkAuthenticationNumber(email, code); @@ -337,7 +329,7 @@ void join_process_failure_12() .willReturn(mimeMessage); // When & Then - loginService.sendAuthenticationNumber(email, purpose); + loginService.sendAuthenticationNumber(email, "SIGN_UP"); String code = codeRepository.findByEmail(email).get().getCode(); loginService.checkAuthenticationNumber(email, code); @@ -355,30 +347,19 @@ void join_process_failure_12() void join_process_failure_13() { // Given - Profile profile = Profile.builder() - .email("another@korea.ac.kr") - .password("test1234") - .username("test1234") - .student_id("1234567") - .degree("MASTER") - .semester(1) - .department("test") - .point(0) - .access_expiration_date(LocalDate.now()) - .created_at(LocalDate.now()) - .role("ROLE_USER") - .build(); - profileRepository.save(profile); - given(javaMailSender.createMimeMessage()) .willReturn(mimeMessage); + profileDto.setEmail("another_email@korea.ac.kr"); + createAccount("another_email@korea.ac.kr"); + // When & Then - loginService.sendAuthenticationNumber(email, purpose); + loginService.sendAuthenticationNumber(email, "SIGN_UP"); String code = codeRepository.findByEmail(email).get().getCode(); loginService.checkAuthenticationNumber(email, code); + profileDto.setEmail(email); profileDto.setCode(code); AppException exception = assertThrows( AppException.class, @@ -398,7 +379,7 @@ void join_process_failure_14() .willReturn(mimeMessage); // When & Then - loginService.sendAuthenticationNumber(email, purpose); + loginService.sendAuthenticationNumber(email, "SIGN_UP"); String code = codeRepository.findByEmail(email).get().getCode(); loginService.checkAuthenticationNumber(email, code); @@ -410,4 +391,164 @@ void join_process_failure_14() ); assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.INVALID_DEGREE); } + + @Test + @DisplayName("임시 비밀번호 발급 프로세스 성공") + void resetPassword_process_success() throws Exception + { + // Given + createAccount(email); + + // When & Then + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("email", profileDto.getEmail()) + .param("password", profileDto.getPassword()) + .param("remember-me", "true")) + .andExpect(status().isOk()); + + loginService.sendAuthenticationNumber(email, "RESET_PASSWORD"); + + String code = codeRepository.findByEmail(email).get().getCode(); + loginService.resetPassword( + ProfileDto.ResetPassword.builder() + .email(profileDto.getEmail()) + .code(code) + .build() + ); + + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("email", profileDto.getEmail()) + .param("password", profileDto.getPassword()) + .param("remember-me", "true")) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("임시 비밀번호 발급 프로세스 실패 1 : 해당 이메일의 계정이 존재하지 않는 경우") + void resetPassword_process_failure_1() throws Exception + { + // Given + given(javaMailSender.createMimeMessage()) + .willReturn(mimeMessage); + + // When + loginService.sendAuthenticationNumber(email, "RESET_PASSWORD"); + + // Then + assertThat(codeRepository.findByEmail(email)).isEqualTo(Optional.empty()); + } + + @Test + @DisplayName("임시 비밀번호 발급 프로세스 실패 2 : 인증번호가 틀린 경우") + void resetPassword_process_failure_2() throws Exception + { + // Given + createAccount(email); + + // When & Then + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("email", profileDto.getEmail()) + .param("password", profileDto.getPassword()) + .param("remember-me", "true")) + .andExpect(status().isOk()); + + loginService.sendAuthenticationNumber(email, "RESET_PASSWORD"); + + AppException exception = assertThrows( + AppException.class, + () -> loginService.resetPassword( + ProfileDto.ResetPassword.builder() + .email(profileDto.getEmail()) + .code("wrong_code") + .build() + ) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.WRONG_CODE); + } + + @Test + @DisplayName("로그인 여부 확인 성공") + void checkLogin_success() + { + // Given + createAccount(email); + + Principal mockPrincipal = mock(Principal.class); + given(mockPrincipal.getName()).willReturn(email); + + // When + ProfileDto.CheckLogin result = loginService.checkLogin(mockPrincipal); + + // Then + assertThat(result.getEmail()).isEqualTo(email); + } + + @Test + @DisplayName("로그인 여부 확인 실패 : 계정이 존재하지 않는 경우") + void checkLogin_failure() + { + // Given + Principal mockPrincipal = mock(Principal.class); + given(mockPrincipal.getName()).willReturn("does_not_exist@naver.com"); + + // When & Then + AppException exception = assertThrows( + AppException.class, + () -> loginService.checkLogin(mockPrincipal) + ); + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.EMAIL_NOT_FOUND); + } + + @Test + @DisplayName("Access token 재발급 성공") + void refreshToken_success() throws Exception + { + // Given + createAccount(email); + + MvcResult loginResult = mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("email", profileDto.getEmail()) + .param("password", profileDto.getPassword()) + .param("remember-me", "true")) + .andExpect(status().isOk()) + .andReturn(); + + String jsonResponse = loginResult.getResponse().getContentAsString(); + ObjectMapper objectMapper = new ObjectMapper(); + Map responseMap = objectMapper.readValue(jsonResponse, new TypeReference<>() {}); + Map dataMap = (Map) responseMap.get("data"); + + String refreshToken = dataMap.get("refreshToken"); + + HttpServletRequest mockHttpServletRequest = mock(HttpServletRequest.class); + given(mockHttpServletRequest.getHeader("Authorization")).willReturn("Bearer " + refreshToken); + + // When + String newAccessToken = loginService.refreshToken(mockHttpServletRequest); + + // Then + assertThat(newAccessToken).isNotEmpty(); + assertThat(newAccessToken).isNotNull(); + } + + @Test + @DisplayName("Access token 재발급 실패 : 올바르지 않은 refresh token") + void refreshToken_failure() + { + // Given + String refreshToken = "thisIsNotOfficialRefreshToken"; + + HttpServletRequest mockHttpServletRequest = mock(HttpServletRequest.class); + given(mockHttpServletRequest.getHeader("Authorization")).willReturn("Bearer " + refreshToken); + + // When & Then + assertThrows( + AppException.class, + () -> loginService.refreshToken(mockHttpServletRequest) + ); + } } \ No newline at end of file From 7544682043b52504ced8b6e901404ead38b6e00a Mon Sep 17 00:00:00 2001 From: kimyerak Date: Sun, 2 Feb 2025 22:35:41 +0900 Subject: [PATCH 09/11] feat: course basic dto --- .../com/example/Devkor_project/dto/CourseDto.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/com/example/Devkor_project/dto/CourseDto.java b/src/main/java/com/example/Devkor_project/dto/CourseDto.java index 24aab99..cecebd0 100644 --- a/src/main/java/com/example/Devkor_project/dto/CourseDto.java +++ b/src/main/java/com/example/Devkor_project/dto/CourseDto.java @@ -877,6 +877,18 @@ public static CourseDto.UPDATED_ExpiredDetail UPDATED_entityToExpiredDetail( .isBookmark(isBookmark) .build(); } + @AllArgsConstructor + @NoArgsConstructor + @Getter + @ToString + @Builder + public static class Basic { // ✅ 내부 static 클래스로 선언되어 있어야 함 + private Long course_id; + private String course_code; + private String name; + private String professor; + } + public static CourseDto.Basic fromCourse(Course course) { return CourseDto.Basic.builder() .course_id(course.getCourse_id()) From f81fe77dff94bd3e202e388e4b698af75029e73e Mon Sep 17 00:00:00 2001 From: kimyerak Date: Mon, 3 Feb 2025 03:45:39 +0900 Subject: [PATCH 10/11] =?UTF-8?q?=EC=95=84=EC=A7=81=20=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=ED=91=9C=20=EC=82=AD=EC=A0=9C=20=EB=82=A8=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PrivacyController.java | 14 +++- .../controller/TimetableController.java | 22 ++--- .../dto/CourseAssignmentDto.java | 7 +- .../dto/PrivacyAssignmentDto.java | 3 +- .../Devkor_project/dto/PrivacyDto.java | 6 +- .../Devkor_project/dto/TimeLocationDto.java | 17 ++++ .../Devkor_project/entity/Timetable.java | 2 + .../Devkor_project/exception/ErrorCode.java | 4 +- .../repository/TimeLocationRepository.java | 10 +++ .../service/TimetableService.java | 84 ++++++++++++++++--- .../Devkor_project/util/TimeLocationUtil.java | 56 +++++++++++++ 11 files changed, 192 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/example/Devkor_project/dto/TimeLocationDto.java create mode 100644 src/main/java/com/example/Devkor_project/util/TimeLocationUtil.java diff --git a/src/main/java/com/example/Devkor_project/controller/PrivacyController.java b/src/main/java/com/example/Devkor_project/controller/PrivacyController.java index 526386f..298fdd7 100644 --- a/src/main/java/com/example/Devkor_project/controller/PrivacyController.java +++ b/src/main/java/com/example/Devkor_project/controller/PrivacyController.java @@ -38,7 +38,7 @@ public List getAllPrivacy(Principal principal) { @Operation(summary = "특정 시간표의 개인일정 조회") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") @Parameter(in = ParameterIn.PATH, name = "timetableId", description = "조회할 시간표 ID") - public List getPrivacyByTimetable(@PathVariable Long timetableId, Principal principal) { + public List getPrivacyByTimetable(@PathVariable ("timetableId")Long timetableId, Principal principal) { return privacyService.getPrivacyByTimetable(timetableId, principal); } @@ -51,7 +51,7 @@ public List getPrivacyByTimetable(@PathVariable Long timetableId, Pr // } @PostMapping - @Operation(summary = "개인일정 생성") + @Operation(summary = "개인일정 생성(요일이름 반드시 한글자로 할것!!! ex. 월, 수, 토)") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") public ResponseEntity createPrivacy(@Valid @RequestBody PrivacyDto privacyDto, Principal principal) { PrivacyDto createdPrivacy = privacyService.createPrivacy(privacyDto, principal); @@ -69,15 +69,21 @@ public ResponseEntity createPrivacy(@Valid @RequestBody Pri @Operation(summary = "개인일정 수정 (이름 & 장소만 변경 가능)") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") @Parameter(in = ParameterIn.PATH, name = "privacyId", description = "수정할 개인일정 ID") - public ResponseEntity updatePrivacy(@PathVariable Long privacyId, @Valid @RequestBody PrivacyUpdateDto privacyUpdateDto, Principal principal) { + public ResponseEntity updatePrivacy(@PathVariable ("privacyId")Long privacyId, @Valid @RequestBody PrivacyUpdateDto privacyUpdateDto, Principal principal) { return privacyService.updatePrivacy(privacyId, privacyUpdateDto, principal); } @DeleteMapping("/{privacyId}") @Operation(summary = "개인일정 삭제") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") - public void deletePrivacy(@PathVariable Long privacyId, Principal principal) { + public ResponseEntity deletePrivacy(@PathVariable ("privacyId")Long privacyId, Principal principal) { privacyService.deletePrivacy(privacyId, principal); + return ResponseEntity.status(HttpStatus.OK) + .body(ResponseDto.Success.builder() + .message("개인 일정이 성공적으로 삭제되었습니다.") + .version("v1.2.1-alpha") + .build()); + } // @PostMapping("/{timetableId}/privacy") diff --git a/src/main/java/com/example/Devkor_project/controller/TimetableController.java b/src/main/java/com/example/Devkor_project/controller/TimetableController.java index 30cc49a..9683820 100644 --- a/src/main/java/com/example/Devkor_project/controller/TimetableController.java +++ b/src/main/java/com/example/Devkor_project/controller/TimetableController.java @@ -44,27 +44,27 @@ public ResponseEntity createTimetable( } /** 🟢 강의 추가 */ - @PostMapping("/{timetableId}/course") + @PostMapping("/{timetableId}/course/{courseId}") @Operation(summary = "시간표에 강의 추가") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") - public ResponseEntity addCourseToTimetable(@PathVariable Long timetableId, @Valid @RequestBody CourseAssignmentDto requestDto, Principal principal) { - return timetableService.addCourseToTimetable(timetableId, requestDto, principal); + public ResponseEntity addCourseToTimetable(@PathVariable("timetableId") Long timetableId, @PathVariable("courseId") Long courseId, Principal principal) { + return timetableService.addCourseToTimetable(timetableId, courseId, principal); } /** 🟢 강의 제거 */ @DeleteMapping("/{timetableId}/course/{courseId}") @Operation(summary = "시간표에서 강의 제거") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") - public ResponseEntity removeCourseFromTimetable(@PathVariable Long timetableId, @PathVariable Long courseId, Principal principal) { + public ResponseEntity removeCourseFromTimetable(@PathVariable("timetableId") Long timetableId, @PathVariable ("courseId")Long courseId, Principal principal) { return timetableService.removeCourseFromTimetable(timetableId, courseId, principal); } /** 🟢 개인 일정 추가 */ - @PostMapping("/{timetableId}/privacy") + @PostMapping("/{timetableId}/privacy/{privacyId}") @Operation(summary = "시간표에 개인 일정 추가(이렇게 추후에 추가해도 되고, privacy 애초에 POST할때 시간표 id지정해서 넣어주는 것도 가능") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") - public ResponseEntity addPrivacyToTimetable(@PathVariable Long timetableId, @Valid @RequestBody PrivacyAssignmentDto requestDto, Principal principal) { - return timetableService.addPrivacyToTimetable(timetableId, requestDto, principal); + public ResponseEntity addPrivacyToTimetable(@PathVariable("timetableId") Long timetableId, @PathVariable("privacyId") Long privacyId, Principal principal) { + return timetableService.addPrivacyToTimetable(timetableId, privacyId, principal); } // /** 🟢 로그인된 사용자의 모든 시간표 조회 */ @@ -88,7 +88,7 @@ public ResponseEntity getTimetableNamesAndIds(Principal pri @GetMapping("/{timetableId}") @Operation(summary = "특정 시간표 조회") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") - public ResponseEntity getTimetableById(@PathVariable Long timetableId, Principal principal) { + public ResponseEntity getTimetableById(@PathVariable("timetableId") Long timetableId, Principal principal) { return timetableService.getTimetableByIdWithDetails(timetableId, principal); } @@ -97,8 +97,8 @@ public ResponseEntity getTimetableById(@PathVariable Long t @Operation(summary = "시간표에서 개인 일정 제거") @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") public ResponseEntity removePrivacyFromTimetable( - @PathVariable Long timetableId, - @PathVariable Long privacyId, + @PathVariable ("timetableId")Long timetableId, + @PathVariable ("privacyId")Long privacyId, Principal principal ) { return timetableService.removePrivacyFromTimetable(timetableId, privacyId, principal); @@ -110,7 +110,7 @@ public ResponseEntity removePrivacyFromTimetable( @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") @Parameter(in = ParameterIn.PATH, name = "timetableId", description = "이름을 변경할 시간표 ID") public ResponseEntity updateTimetableName( - @PathVariable Long timetableId, + @PathVariable ("timetableId") Long timetableId, @RequestBody TimetableDto timetableDto, Principal principal ) { diff --git a/src/main/java/com/example/Devkor_project/dto/CourseAssignmentDto.java b/src/main/java/com/example/Devkor_project/dto/CourseAssignmentDto.java index 720c2d5..4473ad7 100644 --- a/src/main/java/com/example/Devkor_project/dto/CourseAssignmentDto.java +++ b/src/main/java/com/example/Devkor_project/dto/CourseAssignmentDto.java @@ -2,9 +2,14 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import lombok.Getter; +import lombok.*; @Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "시간표에 강의를 추가하기 위한 요청 DTO") public class CourseAssignmentDto { @NotNull(message = "courseId는 null일 수 없습니다.") diff --git a/src/main/java/com/example/Devkor_project/dto/PrivacyAssignmentDto.java b/src/main/java/com/example/Devkor_project/dto/PrivacyAssignmentDto.java index a3d8491..d73d803 100644 --- a/src/main/java/com/example/Devkor_project/dto/PrivacyAssignmentDto.java +++ b/src/main/java/com/example/Devkor_project/dto/PrivacyAssignmentDto.java @@ -4,9 +4,10 @@ import jakarta.validation.constraints.NotNull; import lombok.*; +@Getter +@Setter @AllArgsConstructor @NoArgsConstructor -@Getter @Builder @Schema(description = "시간표에 개인 일정을 추가하기 위한 요청 DTO") public class PrivacyAssignmentDto { diff --git a/src/main/java/com/example/Devkor_project/dto/PrivacyDto.java b/src/main/java/com/example/Devkor_project/dto/PrivacyDto.java index 3d90213..d5e2ffa 100644 --- a/src/main/java/com/example/Devkor_project/dto/PrivacyDto.java +++ b/src/main/java/com/example/Devkor_project/dto/PrivacyDto.java @@ -4,8 +4,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import com.example.Devkor_project.entity.Privacy; -import lombok.Builder; -import lombok.Getter; +import lombok.*; import java.time.LocalTime; import java.time.LocalDateTime; @@ -13,6 +12,9 @@ @Getter @Builder +@Setter +@NoArgsConstructor +@AllArgsConstructor public class PrivacyDto { @Schema(description = "Privacy ID") diff --git a/src/main/java/com/example/Devkor_project/dto/TimeLocationDto.java b/src/main/java/com/example/Devkor_project/dto/TimeLocationDto.java new file mode 100644 index 0000000..85aa512 --- /dev/null +++ b/src/main/java/com/example/Devkor_project/dto/TimeLocationDto.java @@ -0,0 +1,17 @@ +package com.example.Devkor_project.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TimeLocationDto { + private String day; // 요일 (월, 화, 수 등) + private Integer startPeriod; // 시작 교시 (or 시작 시간) + private Integer endPeriod; // 끝 교시 (or 끝 시간) + private String location; // 장소 (강의실 or 개인 일정 장소) +} diff --git a/src/main/java/com/example/Devkor_project/entity/Timetable.java b/src/main/java/com/example/Devkor_project/entity/Timetable.java index d54d214..53518e3 100644 --- a/src/main/java/com/example/Devkor_project/entity/Timetable.java +++ b/src/main/java/com/example/Devkor_project/entity/Timetable.java @@ -61,4 +61,6 @@ protected void onCreate() { protected void onUpdate() { this.updatedAt = LocalDateTime.now(); } + + } diff --git a/src/main/java/com/example/Devkor_project/exception/ErrorCode.java b/src/main/java/com/example/Devkor_project/exception/ErrorCode.java index b9c0c87..b6e7817 100644 --- a/src/main/java/com/example/Devkor_project/exception/ErrorCode.java +++ b/src/main/java/com/example/Devkor_project/exception/ErrorCode.java @@ -46,7 +46,9 @@ public enum ErrorCode TIMETABLE_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 시간표를 찾을 수 없습니다."), PRIVACY_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 개인 일정을 찾을 수 없습니다."), DUPLICATE_ENTRY(HttpStatus.BAD_REQUEST, "이미 추가된 일정입니다."), - SCHEDULE_CONFLICT(HttpStatus.CONFLICT, "다른 일정과 시간이 겹칩니다."); + SCHEDULE_CONFLICT(HttpStatus.CONFLICT, "다른 일정과 시간이 겹칩니다."), + INVALID_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), + ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/example/Devkor_project/repository/TimeLocationRepository.java b/src/main/java/com/example/Devkor_project/repository/TimeLocationRepository.java index eb9b9d6..b44c17d 100644 --- a/src/main/java/com/example/Devkor_project/repository/TimeLocationRepository.java +++ b/src/main/java/com/example/Devkor_project/repository/TimeLocationRepository.java @@ -14,4 +14,14 @@ public interface TimeLocationRepository extends JpaRepository findByCourseId(Long course_id); + + @Query(value = "SELECT tl.* FROM time_location tl " + + "JOIN course c ON tl.course_id = c.course_id " + + "JOIN timetable_courses tc ON c.course_id = tc.course_id " + + "WHERE tc.timetable_id = :timetable_id", nativeQuery = true) + List findByTimetableId(@Param("timetable_id") Long timetableId); + + // 📌 여러 개의 course_id를 한 번에 조회하는 메서드 추가 + @Query("SELECT tl FROM TimeLocation tl WHERE tl.course_id.course_id IN :courseIds") + List findByCourseIds(@Param("courseIds") List courseIds); } diff --git a/src/main/java/com/example/Devkor_project/service/TimetableService.java b/src/main/java/com/example/Devkor_project/service/TimetableService.java index e3f76be..9d49f30 100644 --- a/src/main/java/com/example/Devkor_project/service/TimetableService.java +++ b/src/main/java/com/example/Devkor_project/service/TimetableService.java @@ -1,16 +1,15 @@ package com.example.Devkor_project.service; import com.example.Devkor_project.dto.*; -import com.example.Devkor_project.entity.Course; -import com.example.Devkor_project.entity.Privacy; -import com.example.Devkor_project.entity.Profile; -import com.example.Devkor_project.entity.Timetable; +import com.example.Devkor_project.entity.*; import com.example.Devkor_project.exception.AppException; import com.example.Devkor_project.exception.ErrorCode; import com.example.Devkor_project.repository.CourseRepository; import com.example.Devkor_project.repository.PrivacyRepository; import com.example.Devkor_project.repository.ProfileRepository; import com.example.Devkor_project.repository.TimetableRepository; +import com.example.Devkor_project.repository.TimeLocationRepository; +import com.example.Devkor_project.util.TimeLocationUtil; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -18,6 +17,7 @@ import org.springframework.stereotype.Service; import java.security.Principal; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -26,6 +26,7 @@ public class TimetableService { private final TimetableRepository timetableRepository; + private final TimeLocationRepository timeLocationRepository; private final CourseRepository courseRepository; private final PrivacyRepository privacyRepository; private final ProfileRepository profileRepository; @@ -115,11 +116,42 @@ public ResponseEntity getTimetableByIdWithDetails(Long time * 시간표에 강의 추가 */ @Transactional - public ResponseEntity addCourseToTimetable(Long timetableId, CourseAssignmentDto requestDto, Principal principal) { + public ResponseEntity addCourseToTimetable(Long timetableId, Long courseId, Principal principal) { Timetable timetable = validateProfileOwnership(timetableId, principal); - Course course = courseRepository.findById(requestDto.getCourseId()) + + if (courseId == null) { + throw new AppException(ErrorCode.INVALID_REQUEST, "courseId는 null일 수 없습니다."); + } + + Course course = courseRepository.findById(courseId) .orElseThrow(() -> new AppException(ErrorCode.COURSE_NOT_FOUND, "해당 강의를 찾을 수 없습니다.")); + if (timetable.getCourses() == null) { + timetable.setCourses(new ArrayList<>()); // 🛠️ Null 체크 후 리스트 초기화 + } + if (course.getTimetables() == null) { + course.setTimetables(new ArrayList<>()); // 🛠️ Null 체크 후 리스트 초기화 + } + + if (timetable.getCourses().contains(course)) { + throw new AppException(ErrorCode.DUPLICATE_ENTRY, "해당 강의는 이미 추가되어 있습니다."); + } + + // ⏳ 기존 시간표의 강의 및 개인 일정 시간 정보 가져오기 + List existingTimeLocations = TimeLocationUtil.extractFromTimetable(timetable, timeLocationRepository); + + // 🕒 새 강의의 시간표 정보 가져오기 + List newCourseTimeLocations = timeLocationRepository.findByCourseIds(List.of(course.getCourse_id())) + .stream() + .map(tl -> new TimeLocationDto(tl.getDay(), tl.getStartPeriod(), tl.getEndPeriod(), tl.getLocation())) + .toList(); + + // ⚠️ 기존 일정과 새 강의 시간이 겹치는지 확인 + if (TimeLocationUtil.hasScheduleConflict(existingTimeLocations, newCourseTimeLocations)) { + throw new AppException(ErrorCode.SCHEDULE_CONFLICT, "시간표에 이미 같은 시간에 다른 강의 또는 개인 일정이 있습니다."); + } + + // 📌 겹치지 않으면 추가 진행 timetable.getCourses().add(course); course.getTimetables().add(timetable); @@ -129,10 +161,11 @@ public ResponseEntity addCourseToTimetable(Long timetableId return ResponseEntity.status(HttpStatus.OK) .body(ResponseDto.Success.builder() .message("강의가 시간표에 추가되었습니다.") - .version("v1.1.4") + .version("v1.2.1-alpha") .build()); } + /** * 시간표에서 강의 제거 */ @@ -157,18 +190,42 @@ public ResponseEntity removeCourseFromTimetable(Long timeta /** 🟢 시간표에 개인 일정 추가 (중복은 방지되도록 코드 수정 완) */ @Transactional - public ResponseEntity addPrivacyToTimetable( - Long timetableId, PrivacyAssignmentDto requestDto, Principal principal) { - + public ResponseEntity addPrivacyToTimetable(Long timetableId, Long privacyId, Principal principal) { Timetable timetable = validateProfileOwnership(timetableId, principal); - Privacy privacy = privacyRepository.findById(requestDto.getPrivacyId()) + + if (privacyId == null) { + throw new AppException(ErrorCode.INVALID_REQUEST, "privacyId는 null일 수 없습니다."); + } + + Privacy privacy = privacyRepository.findById(privacyId) .orElseThrow(() -> new AppException(ErrorCode.PRIVACY_NOT_FOUND, "해당 개인 일정을 찾을 수 없습니다.")); + if (timetable.getPrivacies() == null) { + timetable.setPrivacies(new ArrayList<>()); // 🛠️ Null 체크 후 리스트 초기화 + } + if (privacy.getTimetables() == null) { + privacy.setTimetables(new ArrayList<>()); // 🛠️ Null 체크 후 리스트 초기화 + } + if (timetable.getPrivacies().contains(privacy)) { throw new AppException(ErrorCode.DUPLICATE_ENTRY, "해당 개인 일정은 이미 추가되어 있습니다."); } - // 시간표에 개인 일정 추가 + // ⏳ 기존 시간표의 강의 및 개인 일정 시간 정보 가져오기 + List existingTimeLocations = TimeLocationUtil.extractFromTimetable(timetable, timeLocationRepository); + + // 🕒 새 개인 일정의 시간표 정보 생성 + List newPrivacyTimeLocations = List.of( + new TimeLocationDto(privacy.getDay(), privacy.getStartTime().getHour(), + privacy.getFinishTime().getHour(), privacy.getLocation()) + ); + + // ⚠️ 기존 일정과 새 일정 시간이 겹치는지 확인 + if (TimeLocationUtil.hasScheduleConflict(existingTimeLocations, newPrivacyTimeLocations)) { + throw new AppException(ErrorCode.SCHEDULE_CONFLICT, "시간표에 이미 같은 시간에 다른 강의 또는 개인 일정이 있습니다."); + } + + // 📌 겹치지 않으면 추가 진행 timetable.getPrivacies().add(privacy); privacy.getTimetables().add(timetable); @@ -178,11 +235,12 @@ public ResponseEntity addPrivacyToTimetable( return ResponseEntity.status(HttpStatus.OK) .body(ResponseDto.Success.builder() .message("개인 일정이 시간표에 추가되었습니다.") - .version("v1.1.4") + .version("v1.2.1-alpha") .build()); } + /** * 시간표에서 개인 일정 제거 */ diff --git a/src/main/java/com/example/Devkor_project/util/TimeLocationUtil.java b/src/main/java/com/example/Devkor_project/util/TimeLocationUtil.java new file mode 100644 index 0000000..e8d256d --- /dev/null +++ b/src/main/java/com/example/Devkor_project/util/TimeLocationUtil.java @@ -0,0 +1,56 @@ +package com.example.Devkor_project.util; +import com.example.Devkor_project.dto.TimeLocationDto; +import com.example.Devkor_project.entity.Course; +import com.example.Devkor_project.entity.Privacy; +import com.example.Devkor_project.entity.TimeLocation; +import com.example.Devkor_project.dto.TimetableDto; +import com.example.Devkor_project.entity.Timetable; +import com.example.Devkor_project.exception.AppException; +import com.example.Devkor_project.exception.ErrorCode; +import com.example.Devkor_project.repository.TimeLocationRepository; + +import java.util.ArrayList; +import java.util.List; +public class TimeLocationUtil { + + // 🕒 시간표에서 기존 강의 & 개인 일정 가져오기 + public static List extractFromTimetable(Timetable timetable, TimeLocationRepository timeLocationRepository) { + List existingTimeLocations = new ArrayList<>(); // 빈 리스트 생성 (가변 가능) + + // 📌 기존 강의의 시간표 정보 가져오기 + List courseTimeLocations = timeLocationRepository.findByCourseIds( + timetable.getCourses().stream().map(Course::getCourse_id).toList() + ); + + existingTimeLocations.addAll(courseTimeLocations.stream() + .map(tl -> new TimeLocationDto(tl.getDay(), tl.getStartPeriod(), tl.getEndPeriod(), tl.getLocation())) + .toList()); + + // 📌 기존 개인 일정의 시간 정보 추가 + existingTimeLocations.addAll(timetable.getPrivacies().stream() + .map(p -> new TimeLocationDto(p.getDay(), p.getStartTime().getHour(), p.getFinishTime().getHour(), p.getLocation())) + .toList()); + + return existingTimeLocations; + } + + + // ⚠️ **스케줄 충돌 여부 확인 함수 추가** + public static boolean hasScheduleConflict(List existingSchedules, List newSchedules) { + for (TimeLocationDto existing : existingSchedules) { + for (TimeLocationDto newSchedule : newSchedules) { + if (existing.getDay().equals(newSchedule.getDay()) && + isTimeOverlapping(existing.getStartPeriod(), existing.getEndPeriod(), + newSchedule.getStartPeriod(), newSchedule.getEndPeriod())) { + return true; + } + } + } + return false; + } + + // ⏳ 시간이 겹치는지 확인 + private static boolean isTimeOverlapping(int start1, int end1, int start2, int end2) { + return start1 <= end2 && start2 <= end1; + } +} \ No newline at end of file From 70bb2400b8328bc7e599ee07b367b79c28293d6d Mon Sep 17 00:00:00 2001 From: kimyerak Date: Mon, 3 Feb 2025 03:53:12 +0900 Subject: [PATCH 11/11] =?UTF-8?q?feat:=20=EC=8B=9C=EA=B0=84=ED=91=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20api=2014=EA=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/TimetableController.java | 9 ++++++++ .../service/TimetableService.java | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/main/java/com/example/Devkor_project/controller/TimetableController.java b/src/main/java/com/example/Devkor_project/controller/TimetableController.java index 9683820..11deb8d 100644 --- a/src/main/java/com/example/Devkor_project/controller/TimetableController.java +++ b/src/main/java/com/example/Devkor_project/controller/TimetableController.java @@ -116,4 +116,13 @@ public ResponseEntity updateTimetableName( ) { return timetableService.updateTimetableName(timetableId, timetableDto, principal); } + + /** 🟢 시간표 삭제 */ + @DeleteMapping("/{timetableId}") + @Operation(summary = "시간표 삭제") + @Parameter(in = ParameterIn.HEADER, name = "Authorization", description = "Bearer {access token}") + @Parameter(in = ParameterIn.PATH, name = "timetableId", description = "삭제할 시간표 ID") + public ResponseEntity deleteTimetable(@PathVariable ("timetableId") Long timetableId, Principal principal) { + return timetableService.deleteTimetable(timetableId, principal); + } } diff --git a/src/main/java/com/example/Devkor_project/service/TimetableService.java b/src/main/java/com/example/Devkor_project/service/TimetableService.java index 9d49f30..e33746e 100644 --- a/src/main/java/com/example/Devkor_project/service/TimetableService.java +++ b/src/main/java/com/example/Devkor_project/service/TimetableService.java @@ -278,6 +278,27 @@ public ResponseEntity updateTimetableName(Long timetableId, .version("v1.1.4") .build()); } + /** + * 🟢 시간표 삭제 + */ + @Transactional + public ResponseEntity deleteTimetable(Long timetableId, Principal principal) { + Timetable timetable = validateProfileOwnership(timetableId, principal); + + // 📌 시간표에서 모든 강의 및 개인 일정 관계 해제 + timetable.getCourses().forEach(course -> course.getTimetables().remove(timetable)); + timetable.getPrivacies().forEach(privacy -> privacy.getTimetables().remove(timetable)); + + // 📌 시간표 삭제 + timetableRepository.delete(timetable); + + return ResponseEntity.status(HttpStatus.OK) + .body(ResponseDto.Success.builder() + .message("시간표가 성공적으로 삭제되었습니다.") + .version("v1.2.1-alpha") + .build()); + } + /** * Helper Method: Principal을 이용해 Profile 조회