Skip to content

Commit

Permalink
feat: 스터디 개설 V2 API 구현 (#863)
Browse files Browse the repository at this point in the history
* feat: 랜덤 출석번호 생성기를 컴포넌트로 등록

* feat: 스터디 개설 v2 API 구현

* test: 스터디 개설 v2 테스트 추가

* feat: 스터디회차 페치조인해서 조회하는 메서드 추가

* fix: 지연로딩 문제로 인한 페치조인 조회하는 메서드 테스트에 적용

* fix: 누락된 where절 추가
  • Loading branch information
uwoobeat authored Feb 2, 2025
1 parent b66e015 commit 1f4bc90
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.gdschongik.gdsc.domain.studyv2.api;

import com.gdschongik.gdsc.domain.studyv2.application.AdminStudyServiceV2;
import com.gdschongik.gdsc.domain.studyv2.dto.request.StudyCreateRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Admin Study V2", description = "스터디 V2 어드민 API입니다.")
@RestController
@RequestMapping("/admin/studies/v2")
@RequiredArgsConstructor
public class AdminStudyControllerV2 {

private final AdminStudyServiceV2 adminStudyServiceV2;

@Operation(summary = "스터디 개설", description = "스터디를 개설합니다. 빈 스터디회차를 함께 생성합니다.")
@PostMapping
public ResponseEntity<Void> createStudy(@Valid @RequestBody StudyCreateRequest request) {
adminStudyServiceV2.createStudy(request);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.gdschongik.gdsc.domain.studyv2.application;

import static com.gdschongik.gdsc.global.exception.ErrorCode.*;

import com.gdschongik.gdsc.domain.member.dao.MemberRepository;
import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.studyv2.dao.StudyV2Repository;
import com.gdschongik.gdsc.domain.studyv2.domain.AttendanceNumberGenerator;
import com.gdschongik.gdsc.domain.studyv2.domain.StudyFactory;
import com.gdschongik.gdsc.domain.studyv2.domain.StudyV2;
import com.gdschongik.gdsc.domain.studyv2.dto.request.StudyCreateRequest;
import com.gdschongik.gdsc.global.exception.CustomException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class AdminStudyServiceV2 {

private final StudyV2Repository studyV2Repository;
private final MemberRepository memberRepository;
private final StudyFactory studyFactory;
private final AttendanceNumberGenerator attendanceNumberGenerator;

@Transactional
public void createStudy(StudyCreateRequest request) {
Member mentor =
memberRepository.findById(request.mentorId()).orElseThrow(() -> new CustomException(MEMBER_NOT_FOUND));

mentor.assignToMentor();

StudyV2 study = studyFactory.create(
request.type(),
request.title(),
request.description(),
request.descriptionNotionLink(),
request.semester(),
request.totalRound(),
request.dayOfWeek(),
request.startTime(),
request.endTime(),
request.applicationPeriod(),
request.discordChannelId(),
request.discordRoleId(),
mentor,
attendanceNumberGenerator);

memberRepository.save(mentor);
studyV2Repository.save(study);

log.info("[AdminStudyService] 스터디 생성 완료: studyId = {}", study.getId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.gdschongik.gdsc.domain.studyv2.dao;

import com.gdschongik.gdsc.domain.studyv2.domain.StudyV2;
import java.util.Optional;

public interface StudyV2CustomRepository {
Optional<StudyV2> findFetchById(Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.gdschongik.gdsc.domain.studyv2.dao;

import com.gdschongik.gdsc.domain.studyv2.domain.StudyV2;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StudyV2Repository extends JpaRepository<StudyV2, Long>, StudyV2CustomRepository {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.gdschongik.gdsc.domain.studyv2.dao;

import static com.gdschongik.gdsc.domain.studyv2.domain.QStudyV2.*;

import com.gdschongik.gdsc.domain.studyv2.domain.StudyV2;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.Optional;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class StudyV2RepositoryImpl implements StudyV2CustomRepository {

private final JPAQueryFactory queryFactory;

@Override
public Optional<StudyV2> findFetchById(Long id) {
return Optional.ofNullable(queryFactory
.selectFrom(studyV2)
.join(studyV2.studySessions)
.fetchJoin()
.where(studyV2.id.eq(id))
.fetchOne());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import java.security.SecureRandom;
import lombok.SneakyThrows;
import org.springframework.stereotype.Component;

/**
* 네 자리의 랜덤한 출석번호를 생성합니다.
*/
@Component
public class RandomAttendanceNumberGenerator implements AttendanceNumberGenerator {

public static final int MIN_ORIGIN = 1000;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.gdschongik.gdsc.domain.studyv2.dto.request;

import com.gdschongik.gdsc.domain.common.vo.Period;
import com.gdschongik.gdsc.domain.common.vo.Semester;
import com.gdschongik.gdsc.domain.study.domain.StudyType;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import java.time.DayOfWeek;
import java.time.LocalTime;

public record StudyCreateRequest(
@NotNull @Positive Long mentorId,
@NotNull StudyType type,
@NotNull String title,
String description,
String descriptionNotionLink,
@NotNull Semester semester,
@NotNull @Positive Integer totalRound,
DayOfWeek dayOfWeek,
LocalTime startTime,
LocalTime endTime,
@NotNull Period applicationPeriod,
String discordChannelId,
String discordRoleId) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.gdschongik.gdsc.domain.studyv2.application;

import static com.gdschongik.gdsc.global.common.constant.StudyConstant.*;
import static org.assertj.core.api.Assertions.*;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.study.domain.StudyType;
import com.gdschongik.gdsc.domain.studyv2.dao.StudyV2Repository;
import com.gdschongik.gdsc.domain.studyv2.domain.StudySessionV2;
import com.gdschongik.gdsc.domain.studyv2.domain.StudyV2;
import com.gdschongik.gdsc.domain.studyv2.dto.request.StudyCreateRequest;
import com.gdschongik.gdsc.helper.IntegrationTest;
import java.util.Optional;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

class AdminStudyServiceV2Test extends IntegrationTest {

@Autowired
AdminStudyServiceV2 adminStudyService;

@Autowired
StudyV2Repository studyV2Repository;

@Nested
class 스터디_생성할때 {

@Test
void 스터디와_스터디회차가_모두_저장된다() {
// given
createRegularMember();
int totalRound = 8;
var request = new StudyCreateRequest(
1L,
StudyType.OFFLINE,
STUDY_TITLE,
STUDY_DESCRIPTION,
STUDY_DESCRIPTION_NOTION_LINK,
STUDY_SEMESTER,
totalRound,
DAY_OF_WEEK,
STUDY_START_TIME,
STUDY_END_TIME,
STUDY_APPLICATION_PERIOD,
STUDY_DISCORD_CHANNEL_ID,
STUDY_DISCORD_ROLE_ID);

// when
adminStudyService.createStudy(request);

// then
Optional<StudyV2> optionalStudy = studyV2Repository.findFetchById(1L);
assertThat(optionalStudy).isPresent();

StudyV2 study = optionalStudy.get();
assertThat(study.getStudySessions()).hasSize(totalRound);
}

@Test
void 스터디회차에_출석번호가_생성되어_저장된다() {
// given
createRegularMember();
var request = new StudyCreateRequest(
1L,
StudyType.OFFLINE,
STUDY_TITLE,
STUDY_DESCRIPTION,
STUDY_DESCRIPTION_NOTION_LINK,
STUDY_SEMESTER,
TOTAL_ROUND,
DAY_OF_WEEK,
STUDY_START_TIME,
STUDY_END_TIME,
STUDY_APPLICATION_PERIOD,
STUDY_DISCORD_CHANNEL_ID,
STUDY_DISCORD_ROLE_ID);

// when
adminStudyService.createStudy(request);

// then
StudyV2 study = studyV2Repository.findFetchById(1L).orElseThrow();
assertThat(study.getStudySessions())
.extracting(StudySessionV2::getLessonAttendanceNumber)
.doesNotContainNull();
}

@Test
void 멘토가_멘토_역할로_변경된다() {
// given
createRegularMember();
var request = new StudyCreateRequest(
1L,
StudyType.OFFLINE,
STUDY_TITLE,
STUDY_DESCRIPTION,
STUDY_DESCRIPTION_NOTION_LINK,
STUDY_SEMESTER,
TOTAL_ROUND,
DAY_OF_WEEK,
STUDY_START_TIME,
STUDY_END_TIME,
STUDY_APPLICATION_PERIOD,
STUDY_DISCORD_CHANNEL_ID,
STUDY_DISCORD_ROLE_ID);

// when
adminStudyService.createStudy(request);

// then
Member mentor = memberRepository.findById(1L).orElseThrow();
assertThat(mentor.isMentor()).isTrue();
}
}
}

0 comments on commit 1f4bc90

Please sign in to comment.