diff --git a/module-admin/src/main/java/org/example/domain/curriculum/controller/CurriculumController.java b/module-admin/src/main/java/org/example/domain/curriculum/controller/CurriculumController.java index 77fa193a..28127d12 100644 --- a/module-admin/src/main/java/org/example/domain/curriculum/controller/CurriculumController.java +++ b/module-admin/src/main/java/org/example/domain/curriculum/controller/CurriculumController.java @@ -34,13 +34,6 @@ public ApiResponse createCurriculum(@RequestBody @Valid CreateCurriculumRe return ApiResponse.onCreate(); } -// @GetMapping() -// @Operation(summary = "커리큘럼 목록 조회") -// public ApiResponse getCurriculumList( -// @ParameterObject @ModelAttribute @Valid SearchCurriculumRequest request) { -// return ApiResponse.onSuccess(curriculumService.getCurriculumList(request)); -// } - @GetMapping("/{curriculum-id}") @Operation(summary = "커리큘럼 상세 조회") public ApiResponse getCurriculum(@PathVariable("curriculum-id") Long curriculumId) { diff --git a/module-admin/src/main/java/org/example/domain/curriculum/controller/request/ReorderCurriculumRequest.java b/module-admin/src/main/java/org/example/domain/curriculum/controller/request/ReorderCurriculumRequest.java new file mode 100644 index 00000000..78e581cc --- /dev/null +++ b/module-admin/src/main/java/org/example/domain/curriculum/controller/request/ReorderCurriculumRequest.java @@ -0,0 +1,12 @@ +package org.example.domain.curriculum.controller.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +@Schema(description = "커리큘럼 순서 변경 요청 객체") +public record ReorderCurriculumRequest( + + @Schema(description = "커리큘럼 ID 목록") + List curriculumIdList +) { +} diff --git a/module-admin/src/main/java/org/example/domain/curriculum/repository/ListCurriculumRepository.java b/module-admin/src/main/java/org/example/domain/curriculum/repository/ListCurriculumRepository.java index 616c844b..f4d225d4 100644 --- a/module-admin/src/main/java/org/example/domain/curriculum/repository/ListCurriculumRepository.java +++ b/module-admin/src/main/java/org/example/domain/curriculum/repository/ListCurriculumRepository.java @@ -9,6 +9,7 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; import lombok.RequiredArgsConstructor; +import org.example.domain.curriculum.Curriculum; import org.example.domain.curriculum.controller.response.ListCurriculumDto; import org.springframework.stereotype.Repository; @@ -41,8 +42,27 @@ public List getCurriculumList(Long studyId) { .where(curriculum.study.id.eq(studyId)) .orderBy( curriculum.week.asc(), - curriculum.id.asc() + curriculum.orderNumber.asc() ) .fetch(); } + + public List getCurriculumListOver(Long studyId, int week) { + + return queryFactory + .selectFrom(curriculum) + .where( + curriculum.study.id.eq(studyId), + curriculum.orderNumber.goe( + JPAExpressions + .select(curriculum.orderNumber.min()) + .from(curriculum) + .where( + curriculum.study.id.eq(studyId), + curriculum.week.gt(week)) + ) + ) + .orderBy(curriculum.orderNumber.asc()) + .fetch(); + } } diff --git a/module-admin/src/main/java/org/example/domain/curriculum/service/CreateCurriculumService.java b/module-admin/src/main/java/org/example/domain/curriculum/service/CreateCurriculumService.java index 437dc6e7..a9e5162b 100644 --- a/module-admin/src/main/java/org/example/domain/curriculum/service/CreateCurriculumService.java +++ b/module-admin/src/main/java/org/example/domain/curriculum/service/CreateCurriculumService.java @@ -1,12 +1,19 @@ package org.example.domain.curriculum.service; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.example.api_response.exception.GeneralException; import org.example.api_response.status.ErrorStatus; import org.example.domain.curriculum.Curriculum; import org.example.domain.curriculum.controller.request.CreateCurriculumRequest; +import org.example.domain.curriculum.controller.request.ReorderCurriculumRequest; import org.example.domain.curriculum.controller.request.UpdateCurriculumRequest; import org.example.domain.curriculum.repository.CurriculumRepository; +import org.example.domain.curriculum.repository.ListCurriculumRepository; import org.example.domain.study.Study; import org.example.domain.study.enums.StudyType; import org.example.domain.study.service.CoreStudyService; @@ -21,6 +28,7 @@ public class CreateCurriculumService { private final CoreCurriculumService coreCurriculumService; private final CoreStudyService coreStudyService; + private final ListCurriculumRepository listCurriculumRepository; private final CurriculumRepository curriculumRepository; /** @@ -37,14 +45,22 @@ public void createCurriculum(CreateCurriculumRequest request) { throw new GeneralException(ErrorStatus.BAD_REQUEST, "자율 스터디는 커리큘럼을 생성할 수 없습니다."); } + // 요청 주차의 이후의 모든 커리큘럼 조회 + List curriculumList = listCurriculumRepository.getCurriculumListOver(request.studyId(), request.week()); + + int newOrderNumber = getNewOrderNumber(curriculumList, study); + curriculumList.forEach(Curriculum::increaseOrderNumber); // 순서 재조정 + curriculumRepository.save( Curriculum.builder() .study(study) .title(request.title()) .week(request.week()) .content(request.content()) + .orderNumber(newOrderNumber) .build() ); + } /** @@ -64,14 +80,30 @@ public void updateCurriculum(Long curriculumId, UpdateCurriculumRequest request) throw new GeneralException(ErrorStatus.BAD_REQUEST, "자율 스터디는 커리큘럼을 생성할 수 없습니다."); } } + + int newOrderNumber = curriculum.getOrderNumber(); // 주차 변경 없으면 기존 순서 유지 + if (!curriculum.getWeek().equals(request.week())) { // 주차 변경 시 순서 재조정 + // 요청 주차의 이후의 모든 커리큘럼 조회 + List curriculumList = listCurriculumRepository.getCurriculumListOver(request.studyId(), request.week()); + newOrderNumber = getNewOrderNumber(curriculumList, study); + curriculumList.forEach(Curriculum::increaseOrderNumber); + } + curriculum.update( study, request.title(), request.week(), - request.content() + request.content(), + newOrderNumber ); } + private int getNewOrderNumber(List curriculumList, Study study) { + return curriculumList.isEmpty() ? + Optional.ofNullable(curriculumRepository.findMaxOrderNumberByStudy(study)).orElse(0) + 1 : + curriculumList.get(0).getOrderNumber(); + } + /** * 커리큘럼 삭제 */ @@ -79,4 +111,35 @@ public void deleteCurriculum(Long curriculumId) { Curriculum curriculum = coreCurriculumService.findById(curriculumId); curriculumRepository.delete(curriculum); } + + /** + * 커리큘럼 순서 변경 + */ + public void reorderCurriculum(Long studyId, ReorderCurriculumRequest request) { + List curriculumList = curriculumRepository.findAllById(request.curriculumIdList()); + if (request.curriculumIdList().size() != curriculumRepository.findAllByStudy(coreStudyService.findById(studyId)).size()) + throw new GeneralException(ErrorStatus.BAD_REQUEST, "모든 커리큘럼을 포함해야 합니다."); + + // 조회한 커리큘럼 목록을 요청 순서대로 재정렬 + Map idMap = curriculumList.stream().collect(Collectors.toMap(Curriculum::getId, Function.identity())); + List orderedCurriculumList = request.curriculumIdList().stream().map(idMap::get).toList(); + + int orderNumber = 1; + for (int i = 0; i < orderedCurriculumList.size(); i++) { + Curriculum curriculum = orderedCurriculumList.get(i); + if (!curriculum.getStudy().getId().equals(studyId)) + throw new GeneralException(ErrorStatus.BAD_REQUEST, "요청이 올바르지 않습니다. 스터디 ID 및 커리큘럼ID를 확인해주세요."); + if (i > 0 && curriculum.getWeek() < orderedCurriculumList.get(i - 1).getWeek()) { + throw new GeneralException(ErrorStatus.NOTICE_BAD_REQUEST, "주차 정보는 오름차순(같은 숫자 포함)이어야 합니다."); + } + + curriculum.update( + curriculum.getStudy(), + curriculum.getTitle(), + curriculum.getWeek(), + curriculum.getContent(), + orderNumber++ + ); + } + } } diff --git a/module-admin/src/main/java/org/example/domain/curriculum/service/CurriculumService.java b/module-admin/src/main/java/org/example/domain/curriculum/service/CurriculumService.java index 6ad0d7a3..9a87f227 100644 --- a/module-admin/src/main/java/org/example/domain/curriculum/service/CurriculumService.java +++ b/module-admin/src/main/java/org/example/domain/curriculum/service/CurriculumService.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.example.domain.curriculum.controller.request.CreateCurriculumRequest; +import org.example.domain.curriculum.controller.request.ReorderCurriculumRequest; import org.example.domain.curriculum.controller.request.UpdateCurriculumRequest; import org.example.domain.curriculum.controller.response.DetailCurriculumResponse; import org.example.domain.curriculum.controller.response.ListCurriculumResponse; @@ -49,4 +50,11 @@ public void updateCurriculum(Long curriculumId, UpdateCurriculumRequest request) public void deleteCurriculum(Long curriculumId) { createCurriculumService.deleteCurriculum(curriculumId); } + + /** + * 커리큘럼 순서 변경 + */ + public void reorderCurriculum(Long studyId, ReorderCurriculumRequest request) { + createCurriculumService.reorderCurriculum(studyId, request); + } } diff --git a/module-admin/src/main/java/org/example/domain/study/controller/StudyController.java b/module-admin/src/main/java/org/example/domain/study/controller/StudyController.java index ebea2d49..fe85e800 100644 --- a/module-admin/src/main/java/org/example/domain/study/controller/StudyController.java +++ b/module-admin/src/main/java/org/example/domain/study/controller/StudyController.java @@ -7,6 +7,7 @@ import org.example.api_response.ApiResponse; import org.example.domain.attendance.controller.response.ListAttendanceResponse; import org.example.domain.attendance.service.AttendanceService; +import org.example.domain.curriculum.controller.request.ReorderCurriculumRequest; import org.example.domain.curriculum.controller.response.ListCurriculumResponse; import org.example.domain.curriculum.service.CurriculumService; import org.example.domain.member.controller.request.SearchMemberRequest; @@ -98,6 +99,15 @@ public ApiResponse getCurriculumList( return ApiResponse.onSuccess(curriculumService.getCurriculumList(studyId)); } + @PatchMapping("/{study-id}/curriculum/reorder") + @Operation(summary = "정규 스터디 커리큘럼 순서 변경") + public ApiResponse reorderCurriculum( + @PathVariable("study-id") Long studyId, + @RequestBody @Valid ReorderCurriculumRequest request) { + curriculumService.reorderCurriculum(studyId, request); + return ApiResponse.onSuccess(); + } + @GetMapping("/{study-id}/attendance") @Operation(summary = "정규 스터디 출석부 조회") public ApiResponse getAttendanceList( diff --git a/module-core/src/main/java/org/example/domain/curriculum/Curriculum.java b/module-core/src/main/java/org/example/domain/curriculum/Curriculum.java index 1a058c09..fbc35ea2 100644 --- a/module-core/src/main/java/org/example/domain/curriculum/Curriculum.java +++ b/module-core/src/main/java/org/example/domain/curriculum/Curriculum.java @@ -49,6 +49,9 @@ public class Curriculum { @Column(length = 1000000) private String content; + @Comment("순서") + private Integer orderNumber; + @CreatedDate @Column(updatable = false) private LocalDateTime createdTime; @@ -64,17 +67,23 @@ public class Curriculum { private String updatedBy; @Builder - public Curriculum(Study study, String title, Integer week, String content) { + public Curriculum(Study study, String title, Integer week, String content, Integer orderNumber) { this.study = study; this.title = title; this.week = week; this.content = content; + this.orderNumber = orderNumber; } - public void update(Study study, String title, Integer week, String content) { + public void update(Study study, String title, Integer week, String content, Integer orderNumber) { if (study != null) this.study = study; if (StringUtils.hasText(title)) this.title = title; if (week != null) this.week = week; if (StringUtils.hasText(content)) this.content = content; + if (orderNumber != null) this.orderNumber = orderNumber; + } + + public void increaseOrderNumber() { + this.orderNumber++; } } diff --git a/module-core/src/main/java/org/example/domain/curriculum/repository/CurriculumRepository.java b/module-core/src/main/java/org/example/domain/curriculum/repository/CurriculumRepository.java index 60746128..d8bc108b 100644 --- a/module-core/src/main/java/org/example/domain/curriculum/repository/CurriculumRepository.java +++ b/module-core/src/main/java/org/example/domain/curriculum/repository/CurriculumRepository.java @@ -4,8 +4,13 @@ import org.example.domain.curriculum.Curriculum; import org.example.domain.study.Study; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface CurriculumRepository extends JpaRepository { List findAllByStudy(Study study); + + @Query("SELECT MAX(c.orderNumber) FROM Curriculum c WHERE c.study = :study") + Integer findMaxOrderNumberByStudy(@Param("study") Study study); } diff --git a/module-core/src/main/java/org/example/security/SecurityConfig.java b/module-core/src/main/java/org/example/security/SecurityConfig.java index 2846f373..17209b3f 100644 --- a/module-core/src/main/java/org/example/security/SecurityConfig.java +++ b/module-core/src/main/java/org/example/security/SecurityConfig.java @@ -46,6 +46,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(new AntPathRequestMatcher("/email/**")).permitAll() .requestMatchers(new AntPathRequestMatcher("/sms/**")).permitAll() .requestMatchers(new AntPathRequestMatcher("/health/**")).permitAll() + .requestMatchers(HttpMethod.GET, "/board").permitAll() // 랜딩 페이지 .requestMatchers(new AntPathRequestMatcher("/study/count")).permitAll() .requestMatchers(new AntPathRequestMatcher("/generation/max")).permitAll() diff --git a/module-core/src/main/java/org/example/util/SecurityUtils.java b/module-core/src/main/java/org/example/util/SecurityUtils.java index 69c13ea5..f003379b 100644 --- a/module-core/src/main/java/org/example/util/SecurityUtils.java +++ b/module-core/src/main/java/org/example/util/SecurityUtils.java @@ -7,6 +7,8 @@ public class SecurityUtils { + private static final String ANONYMOUS_USER = "anonymousUser"; + private static Authentication getAuthentication() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || authentication.getName() == null) { @@ -19,4 +21,8 @@ public static String getCurrentMemberEmail() { Authentication authentication = getAuthentication(); return authentication.getName(); } + + public static boolean isLoggedIn() { + return !getCurrentMemberEmail().equals(ANONYMOUS_USER); + } } diff --git a/module-user/src/main/java/org/example/domain/board/service/ListBoardService.java b/module-user/src/main/java/org/example/domain/board/service/ListBoardService.java index b0a12a45..e949342c 100644 --- a/module-user/src/main/java/org/example/domain/board/service/ListBoardService.java +++ b/module-user/src/main/java/org/example/domain/board/service/ListBoardService.java @@ -51,9 +51,11 @@ public ListBoardResponse getBoardList(SearchBoardRequest request) { for (ListBoardDto board : boardList) { board.updateCategory(board.getCategory()); - // 정규 스터디 참여 이력 없으면 작성자 이름 미노출 - if (!detailStudyMemberRepository.isRegularStudyMember() - && coreMemberService.findByEmail(SecurityUtils.getCurrentMemberEmail()).getRole().equals(Role.ROLE_USER)) { + // 비로그인 or 정규 스터디 참여 이력 없으면 작성자 이름 미노출 + String currentMemberEmail = SecurityUtils.getCurrentMemberEmail(); + if (!SecurityUtils.isLoggedIn() || + (!detailStudyMemberRepository.isRegularStudyMember() + && coreMemberService.findByEmail(currentMemberEmail).getRole().equals(Role.ROLE_USER))) { board.blindCreatedName(); } } diff --git a/module-user/src/main/java/org/example/domain/curriculum/repository/ListCurriculumRepository.java b/module-user/src/main/java/org/example/domain/curriculum/repository/ListCurriculumRepository.java index d25c706f..aa0810be 100644 --- a/module-user/src/main/java/org/example/domain/curriculum/repository/ListCurriculumRepository.java +++ b/module-user/src/main/java/org/example/domain/curriculum/repository/ListCurriculumRepository.java @@ -31,7 +31,7 @@ public List getCurriculumList(Long studyId) { .where(curriculum.study.id.eq(studyId)) .orderBy( curriculum.week.asc(), - curriculum.id.asc() + curriculum.orderNumber.asc() ) .fetch(); }