diff --git a/src/main/java/com/bcsdlab/bcsdinternalapiv2/activity/service/AdminActivityCategoryService.java b/src/main/java/com/bcsdlab/bcsdinternalapiv2/activity/service/AdminActivityCategoryService.java index eb7e208..a400e87 100644 --- a/src/main/java/com/bcsdlab/bcsdinternalapiv2/activity/service/AdminActivityCategoryService.java +++ b/src/main/java/com/bcsdlab/bcsdinternalapiv2/activity/service/AdminActivityCategoryService.java @@ -39,7 +39,8 @@ public AdminActivityCategoryResponse createCategory(ActivityCategoryCreateReques throw new ActivityException(ActivityExceptionType.CATEGORY_SLUG_DUPLICATED); } - int displayOrder = activityCategoryRepository.findAllByOrderByDisplayOrderAsc().size(); + int displayOrder = DisplayOrders.next(activityCategoryRepository.findAllByOrderByDisplayOrderAsc(), + ActivityCategory::getDisplayOrder); ActivityCategory category = activityCategoryRepository.save(ActivityCategory.builder() .slug(request.slug()) .name(request.name()) diff --git a/src/main/java/com/bcsdlab/bcsdinternalapiv2/curriculum/service/AdminCurriculumService.java b/src/main/java/com/bcsdlab/bcsdinternalapiv2/curriculum/service/AdminCurriculumService.java index 4e75feb..2a35b2b 100644 --- a/src/main/java/com/bcsdlab/bcsdinternalapiv2/curriculum/service/AdminCurriculumService.java +++ b/src/main/java/com/bcsdlab/bcsdinternalapiv2/curriculum/service/AdminCurriculumService.java @@ -8,6 +8,7 @@ import com.bcsdlab.bcsdinternalapiv2.curriculum.model.Curriculum; import com.bcsdlab.bcsdinternalapiv2.curriculum.repository.CurriculumRepository; import com.bcsdlab.bcsdinternalapiv2.global.controller.dto.request.PublishRequest; +import com.bcsdlab.bcsdinternalapiv2.global.util.DisplayOrders; import com.bcsdlab.bcsdinternalapiv2.track.exception.TrackException; import com.bcsdlab.bcsdinternalapiv2.track.exception.TrackExceptionType; import com.bcsdlab.bcsdinternalapiv2.track.model.TrackPage; @@ -59,7 +60,9 @@ public AdminCurriculumSummaryResponse createCurriculum(Long trackPageId, Curricu throw new CurriculumException(CurriculumExceptionType.NAME_REQUIRED); } - int displayOrder = curriculumRepository.findAllByTrackPage_IdOrderByDisplayOrderAsc(trackPageId).size(); + int displayOrder = DisplayOrders.next( + curriculumRepository.findAllByTrackPage_IdOrderByDisplayOrderAsc(trackPageId), + Curriculum::getDisplayOrder); Curriculum curriculum = curriculumRepository.save(Curriculum.builder() .trackPage(trackPage) .name(name) diff --git a/src/main/java/com/bcsdlab/bcsdinternalapiv2/curriculum/service/AdminCurriculumTreeService.java b/src/main/java/com/bcsdlab/bcsdinternalapiv2/curriculum/service/AdminCurriculumTreeService.java index 876c8e6..baff182 100644 --- a/src/main/java/com/bcsdlab/bcsdinternalapiv2/curriculum/service/AdminCurriculumTreeService.java +++ b/src/main/java/com/bcsdlab/bcsdinternalapiv2/curriculum/service/AdminCurriculumTreeService.java @@ -80,7 +80,9 @@ public CurriculumWeekResponse createWeek(Long curriculumId, WeekRequest request, .orElseThrow(() -> new CurriculumException(CurriculumExceptionType.CURRICULUM_NOT_FOUND)); requireCurriculumAccess(memberId, curriculum.getTrackPage()); - int displayOrder = curriculumWeekRepository.findAllByCurriculum_IdOrderByDisplayOrderAsc(curriculumId).size(); + int displayOrder = DisplayOrders.next( + curriculumWeekRepository.findAllByCurriculum_IdOrderByDisplayOrderAsc(curriculumId), + CurriculumWeek::getDisplayOrder); CurriculumWeek week = curriculumWeekRepository.save(CurriculumWeek.builder() .curriculum(curriculum) .weekFrom(request.weekFrom()) @@ -124,7 +126,8 @@ public void reorderWeeks(Long curriculumId, OrderRequest request, Long memberId) public CurriculumTopicResponse createTopic(Long weekId, TopicRequest request, Long memberId) { CurriculumWeek week = findWeekOrThrow(weekId); requireCurriculumAccess(memberId, week.getCurriculum().getTrackPage()); - int displayOrder = curriculumTopicRepository.findAllByWeek_IdOrderByDisplayOrderAsc(weekId).size(); + int displayOrder = DisplayOrders.next(curriculumTopicRepository.findAllByWeek_IdOrderByDisplayOrderAsc(weekId), + CurriculumTopic::getDisplayOrder); CurriculumTopic topic = curriculumTopicRepository.save(CurriculumTopic.builder() .week(week) .title(request.title()) diff --git a/src/main/java/com/bcsdlab/bcsdinternalapiv2/game/service/AdminGameMemberService.java b/src/main/java/com/bcsdlab/bcsdinternalapiv2/game/service/AdminGameMemberService.java index 8b9cae3..04f2d66 100644 --- a/src/main/java/com/bcsdlab/bcsdinternalapiv2/game/service/AdminGameMemberService.java +++ b/src/main/java/com/bcsdlab/bcsdinternalapiv2/game/service/AdminGameMemberService.java @@ -69,7 +69,8 @@ public List attachMembers(Long gameId, GameMembersAttac } } - int nextOrder = gameMemberRepository.findAllByGame_IdOrderByDisplayOrderAsc(gameId).size(); + int nextOrder = DisplayOrders.next(gameMemberRepository.findAllByGame_IdOrderByDisplayOrderAsc(gameId), + GameMember::getDisplayOrder); for (Long memberId : memberIds) { gameMemberRepository.save(GameMember.builder() .game(game) diff --git a/src/main/java/com/bcsdlab/bcsdinternalapiv2/game/service/AdminGameService.java b/src/main/java/com/bcsdlab/bcsdinternalapiv2/game/service/AdminGameService.java index 6740f85..29b5c80 100644 --- a/src/main/java/com/bcsdlab/bcsdinternalapiv2/game/service/AdminGameService.java +++ b/src/main/java/com/bcsdlab/bcsdinternalapiv2/game/service/AdminGameService.java @@ -102,7 +102,8 @@ public AdminGameDetailResponse createGame(GameCreateRequest request, Long member .oneLiner(request.oneLiner()) .track(track) .teamLabel(request.teamLabel()) - .displayOrder((int) gameRepository.count()) + .displayOrder(DisplayOrders.next(gameRepository.findAllByOrderByDisplayOrderAsc(), + Game::getDisplayOrder)) .published(false) .build(); diff --git a/src/main/java/com/bcsdlab/bcsdinternalapiv2/global/util/DisplayOrders.java b/src/main/java/com/bcsdlab/bcsdinternalapiv2/global/util/DisplayOrders.java index 9067176..81b0545 100644 --- a/src/main/java/com/bcsdlab/bcsdinternalapiv2/global/util/DisplayOrders.java +++ b/src/main/java/com/bcsdlab/bcsdinternalapiv2/global/util/DisplayOrders.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.ToIntFunction; /** * {@code display_order} 재부여 순수 함수. 특정 엔티티 타입에 의존하지 않는다 — @@ -22,6 +23,19 @@ public final class DisplayOrders { private DisplayOrders() { } + /** + * 새 항목의 display_order. 삭제가 순서를 다시 매기지 않아 번호에 빈칸이 생기므로, 개수가 아니라 + * 마지막 순서 + 1이다(개수를 쓰면 기존 항목과 순서가 겹친다). + * + * @param siblingsInOrder 같은 부모 아래 형제 항목, display_order 오름차순 + */ + public static int next(List siblingsInOrder, ToIntFunction displayOrder) { + if (siblingsInOrder.isEmpty()) { + return 0; + } + return displayOrder.applyAsInt(siblingsInOrder.getLast()) + 1; + } + /** * @param requestedIds 새 순서대로 나열된 id 목록 (배열 인덱스가 곧 새 display_order) * @param existingIds 현재 그 부모 아래 존재하는 전체 id 집합 diff --git a/src/main/java/com/bcsdlab/bcsdinternalapiv2/home/service/AdminMentorSlotService.java b/src/main/java/com/bcsdlab/bcsdinternalapiv2/home/service/AdminMentorSlotService.java index aeef46c..80442bc 100644 --- a/src/main/java/com/bcsdlab/bcsdinternalapiv2/home/service/AdminMentorSlotService.java +++ b/src/main/java/com/bcsdlab/bcsdinternalapiv2/home/service/AdminMentorSlotService.java @@ -44,7 +44,8 @@ public List addSlot(MentorSlotCreateRequest request) { throw new HomeException(HomeExceptionType.MENTOR_ALREADY_ASSIGNED); } - int nextOrder = (int) mentorSlotRepository.count(); + int nextOrder = DisplayOrders.next(mentorSlotRepository.findAllByOrderByDisplayOrderAsc(), + MentorSlot::getDisplayOrder); mentorSlotRepository.save(MentorSlot.builder().member(member).displayOrder(nextOrder).build()); return getSlots(); } diff --git a/src/main/java/com/bcsdlab/bcsdinternalapiv2/home/service/AdminQnaService.java b/src/main/java/com/bcsdlab/bcsdinternalapiv2/home/service/AdminQnaService.java index 903a5a3..91efba2 100644 --- a/src/main/java/com/bcsdlab/bcsdinternalapiv2/home/service/AdminQnaService.java +++ b/src/main/java/com/bcsdlab/bcsdinternalapiv2/home/service/AdminQnaService.java @@ -35,7 +35,8 @@ public AdminQnaResponse createQnaItem(QnaCreateRequest request) { QnaItem saved = qnaItemRepository.save(QnaItem.builder() .question(request.question()) .answer(request.answer()) - .displayOrder((int) qnaItemRepository.count()) + .displayOrder(DisplayOrders.next(qnaItemRepository.findAllByOrderByDisplayOrderAsc(), + QnaItem::getDisplayOrder)) .published(true) .build()); return AdminQnaResponse.from(saved); diff --git a/src/main/java/com/bcsdlab/bcsdinternalapiv2/track/service/AdminTrackPageMemberService.java b/src/main/java/com/bcsdlab/bcsdinternalapiv2/track/service/AdminTrackPageMemberService.java index 0e55f3d..7a4fb93 100644 --- a/src/main/java/com/bcsdlab/bcsdinternalapiv2/track/service/AdminTrackPageMemberService.java +++ b/src/main/java/com/bcsdlab/bcsdinternalapiv2/track/service/AdminTrackPageMemberService.java @@ -68,7 +68,9 @@ public List attachMembers(Long trackPageId, TrackP } } - int nextOrder = trackPageMemberRepository.findAllByTrackPage_IdOrderByDisplayOrderAsc(trackPageId).size(); + int nextOrder = DisplayOrders.next( + trackPageMemberRepository.findAllByTrackPage_IdOrderByDisplayOrderAsc(trackPageId), + TrackPageMember::getDisplayOrder); for (Long memberId : memberIds) { trackPageMemberRepository.save(TrackPageMember.builder() .trackPage(trackPage) diff --git a/src/main/java/com/bcsdlab/bcsdinternalapiv2/track/service/AdminTrackPageService.java b/src/main/java/com/bcsdlab/bcsdinternalapiv2/track/service/AdminTrackPageService.java index 912b392..ecb3a01 100644 --- a/src/main/java/com/bcsdlab/bcsdinternalapiv2/track/service/AdminTrackPageService.java +++ b/src/main/java/com/bcsdlab/bcsdinternalapiv2/track/service/AdminTrackPageService.java @@ -108,7 +108,8 @@ public AdminTrackPageDetailResponse createTrackPage(TrackPageCreateRequest reque .slug(slug) .displayName(request.displayName()) .tagline(request.tagline()) - .displayOrder((int) trackPageRepository.count()) + .displayOrder(DisplayOrders.next(trackPageRepository.findAllByOrderByDisplayOrderAsc(), + TrackPage::getDisplayOrder)) .published(true) .build(); diff --git a/src/test/java/com/bcsdlab/bcsdinternalapiv2/global/DisplayOrdersTest.java b/src/test/java/com/bcsdlab/bcsdinternalapiv2/global/DisplayOrdersTest.java new file mode 100644 index 0000000..521ea8e --- /dev/null +++ b/src/test/java/com/bcsdlab/bcsdinternalapiv2/global/DisplayOrdersTest.java @@ -0,0 +1,23 @@ +package com.bcsdlab.bcsdinternalapiv2.global; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.bcsdlab.bcsdinternalapiv2.global.util.DisplayOrders; +import java.util.List; +import java.util.function.ToIntFunction; +import org.junit.jupiter.api.Test; + +class DisplayOrdersTest { + + private static final ToIntFunction SELF = Integer::intValue; + + @Test + void 형제가_없으면_0이다() { + assertThat(DisplayOrders.next(List.of(), SELF)).isZero(); + } + + @Test + void 삭제로_빈_번호가_있어도_마지막_순서_다음이다() { + assertThat(DisplayOrders.next(List.of(0, 2), SELF)).isEqualTo(3); + } +} diff --git a/src/test/java/com/bcsdlab/bcsdinternalapiv2/home/AdminQnaAndRecruitLinkIntegrationTest.java b/src/test/java/com/bcsdlab/bcsdinternalapiv2/home/AdminQnaAndRecruitLinkIntegrationTest.java index fa64e7a..5406e42 100644 --- a/src/test/java/com/bcsdlab/bcsdinternalapiv2/home/AdminQnaAndRecruitLinkIntegrationTest.java +++ b/src/test/java/com/bcsdlab/bcsdinternalapiv2/home/AdminQnaAndRecruitLinkIntegrationTest.java @@ -1,5 +1,7 @@ package com.bcsdlab.bcsdinternalapiv2.home; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -21,6 +23,8 @@ import com.bcsdlab.bcsdinternalapiv2.track.model.TrackMaster; import com.bcsdlab.bcsdinternalapiv2.track.repository.TrackMasterRepository; import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -186,4 +190,35 @@ private String login(String studentNumber) throws Exception { .andReturn().getResponse().getContentAsString(); return objectMapper.readTree(body).get("accessToken").asText(); } + + @Test + @DisplayName("Q&A를 지운 뒤 새로 만들면 남은 항목과 순서가 겹치지 않고 맨 뒤에 붙는다") + void 삭제_후_생성한_Q_A는_맨_뒤에_붙는다() throws Exception { + long first = createQna("첫 질문"); + long second = createQna("둘째 질문"); + createQna("셋째 질문"); + + mockMvc.perform(delete("/v1/admin/qna/" + second).header("Authorization", "Bearer " + adminToken)) + .andExpect(status().isNoContent()); + long fourth = createQna("넷째 질문"); + + String list = mockMvc.perform(get("/v1/admin/qna").header("Authorization", "Bearer " + adminToken)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + List orders = new ArrayList<>(); + objectMapper.readTree(list).forEach(item -> orders.add(item.get("displayOrder").asInt())); + assertThat(orders).doesNotHaveDuplicates(); + assertThat(objectMapper.readTree(list).get(0).get("id").asLong()).isEqualTo(first); + assertThat(objectMapper.readTree(list).get(2).get("id").asLong()).isEqualTo(fourth); + } + + private long createQna(String question) throws Exception { + String body = mockMvc.perform(post("/v1/admin/qna") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"question\":\"%s\",\"answer\":\"답변\"}".formatted(question))) + .andExpect(status().isCreated()) + .andReturn().getResponse().getContentAsString(); + return objectMapper.readTree(body).get("id").asLong(); + } }