Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 스터디 V2 팩토리 구현 #855

Merged
merged 8 commits into from
Jan 30, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -19,4 +20,17 @@ public class Semester {

@Enumerated(EnumType.STRING)
private SemesterType semesterType;

@Builder(access = AccessLevel.PRIVATE)
private Semester(int academicYear, SemesterType semesterType) {
this.academicYear = academicYear;
this.semesterType = semesterType;
}

public static Semester of(int academicYear, SemesterType semesterType) {
return Semester.builder()
.academicYear(academicYear)
.semesterType(semesterType)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.gdschongik.gdsc.domain.studyv2.domain;

public interface AttendanceNumberGenerator {
int generate();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.gdschongik.gdsc.domain.studyv2.domain;

import java.security.SecureRandom;
import lombok.SneakyThrows;

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

public static final int MIN_ORIGIN = 1000;
public static final int MAX_BOUND = 10000;

@Override
@SneakyThrows
public int generate() {
return SecureRandom.getInstanceStrong()
.ints(MIN_ORIGIN, MAX_BOUND)
.findFirst()
.orElseThrow();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.gdschongik.gdsc.domain.studyv2.domain;

import com.gdschongik.gdsc.domain.common.vo.Period;
import com.gdschongik.gdsc.domain.common.vo.Semester;
import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.study.domain.StudyType;
import com.gdschongik.gdsc.global.annotation.DomainFactory;
import java.time.DayOfWeek;
import java.time.LocalTime;
import java.util.stream.IntStream;

@DomainFactory
public class StudyFactory {

/**
* 스터디 및 스터디회차를 생성합니다.
* 스터디회차의 경우 총 회차 수만큼 생성되며, 생성 순서에 따라 position 값이 지정됩니다.
*/
public StudyV2 create(
StudyType type,
String title,
String description,
String descriptionNotionLink,
Semester semester,
Integer totalRound,
DayOfWeek dayOfWeek,
LocalTime startTime,
LocalTime endTime,
Period applicationPeriod,
String discordChannelId,
String discordRoleId,
Member mentor,
AttendanceNumberGenerator attendanceNumberGenerator) {
Comment on lines +19 to +33
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

매개변수가 너무 많습니다.

현재 create 메서드는 14개의 매개변수를 가지고 있어 가독성과 유지보수성이 떨어집니다.

다음과 같이 DTO를 사용하는 것을 제안합니다:

@Getter
@Builder
public class StudyCreateCommand {
    private final StudyType type;
    private final String title;
    private final String description;
    private final String descriptionNotionLink;
    private final Semester semester;
    private final Integer totalRound;
    private final DayOfWeek dayOfWeek;
    private final LocalTime startTime;
    private final LocalTime endTime;
    private final Period applicationPeriod;
    private final String discordChannelId;
    private final String discordRoleId;
    private final Member mentor;
}

그리고 메서드 시그니처를 다음과 같이 변경하세요:

public StudyV2 create(StudyCreateCommand command, AttendanceNumberGenerator attendanceNumberGenerator)

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

IntStream.rangeClosed(1, totalRound)
.forEach(round -> StudySessionV2.createEmpty(round, attendanceNumberGenerator.generate(), study));

return study;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public class StudySessionV2 extends BaseEntity {
@Column(name = "study_session_v2_id")
private Long id;

@Comment("회차 순서")
private Integer position;

@Comment("회차 제목")
private String title;

Expand Down Expand Up @@ -70,13 +73,15 @@ public class StudySessionV2 extends BaseEntity {
*/
@Builder(access = AccessLevel.PRIVATE)
private StudySessionV2(
Integer position,
String title,
String description,
Integer lessonAttendanceNumber,
Period lessonPeriod,
String assignmentDescriptionLink,
Period assignmentPeriod,
StudyV2 studyV2) {
this.position = position;
this.title = title;
this.description = description;
this.lessonAttendanceNumber = lessonAttendanceNumber;
Expand All @@ -87,21 +92,10 @@ private StudySessionV2(
studyV2.getStudySessions().add(this);
}

public static void create(
String title,
String description,
Integer lessonAttendanceNumber,
Period lessonPeriod,
String assignmentDescriptionLink,
Period assignmentPeriod,
StudyV2 studyV2) {
public static void createEmpty(Integer position, Integer lessonAttendanceNumber, StudyV2 studyV2) {
StudySessionV2.builder()
.title(title)
.description(description)
.position(position)
.lessonAttendanceNumber(lessonAttendanceNumber)
.lessonPeriod(lessonPeriod)
.assignmentDescriptionLink(assignmentDescriptionLink)
.assignmentPeriod(assignmentPeriod)
.studyV2(studyV2)
.build();
}
Comment on lines +95 to 101
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

팩토리 메서드의 반환 값과 유효성 검사가 필요합니다.

createEmpty 메서드가 void를 반환하고 있으며, 입력값에 대한 유효성 검사가 없습니다.

다음과 같이 수정하는 것을 제안합니다:

-    public static void createEmpty(Integer position, Integer lessonAttendanceNumber, StudyV2 studyV2) {
-        StudySessionV2.builder()
-                .position(position)
-                .lessonAttendanceNumber(lessonAttendanceNumber)
-                .studyV2(studyV2)
-                .build();
+    public static StudySessionV2 createEmpty(Integer position, Integer lessonAttendanceNumber, StudyV2 studyV2) {
+        if (position == null || position <= 0) {
+            throw new IllegalArgumentException("회차 순서는 0보다 커야 합니다.");
+        }
+        if (lessonAttendanceNumber == null) {
+            throw new IllegalArgumentException("출석 번호는 null일 수 없습니다.");
+        }
+        if (studyV2 == null) {
+            throw new IllegalArgumentException("스터디 정보는 null일 수 없습니다.");
+        }
+        
+        return StudySessionV2.builder()
+                .position(position)
+                .lessonAttendanceNumber(lessonAttendanceNumber)
+                .studyV2(studyV2)
+                .build();
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static void createEmpty(Integer position, Integer lessonAttendanceNumber, StudyV2 studyV2) {
StudySessionV2.builder()
.title(title)
.description(description)
.position(position)
.lessonAttendanceNumber(lessonAttendanceNumber)
.lessonPeriod(lessonPeriod)
.assignmentDescriptionLink(assignmentDescriptionLink)
.assignmentPeriod(assignmentPeriod)
.studyV2(studyV2)
.build();
}
public static StudySessionV2 createEmpty(Integer position, Integer lessonAttendanceNumber, StudyV2 studyV2) {
if (position == null || position <= 0) {
throw new IllegalArgumentException("회차 순서는 0보다 커야 합니다.");
}
if (lessonAttendanceNumber == null) {
throw new IllegalArgumentException("출석 번호는 null일 수 없습니다.");
}
if (studyV2 == null) {
throw new IllegalArgumentException("스터디 정보는 null일 수 없습니다.");
}
return StudySessionV2.builder()
.position(position)
.lessonAttendanceNumber(lessonAttendanceNumber)
.studyV2(studyV2)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.gdschongik.gdsc.domain.studyv2.domain;

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.helper.FixtureHelper;
import org.junit.jupiter.api.Test;

class StudyFactoryTest {

FixtureHelper fixtureHelper = new FixtureHelper();
StudyFactory studyFactory = new StudyFactory();

static class FixedAttendanceNumberGenerator implements AttendanceNumberGenerator {
@Override
public int generate() {
return 1000;
}
}

@Test
void 스터디_생성시_설정한_총_회차만큼_스터디회차가_생성된다() {
// given
Member mentor = fixtureHelper.createMentor(1L);
AttendanceNumberGenerator generator = new FixedAttendanceNumberGenerator();
int totalRound = 8;

// when
StudyV2 study = studyFactory.create(
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,
mentor,
generator);

// then
assertThat(study.getStudySessions()).hasSize(8);
}
Comment on lines +23 to +49
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

테스트 케이스 보완이 필요합니다.

현재 테스트는 기본적인 케이스만 다루고 있습니다. 경계값이나 예외 상황에 대한 테스트가 누락되어 있습니다.

다음과 같은 테스트 케이스 추가를 제안드립니다:

@Test
void 총_회차가_0이하일_경우_예외가_발생한다() {
    // given
    Member mentor = fixtureHelper.createMentor(1L);
    AttendanceNumberGenerator generator = new TestAttendanceNumberGenerator(1000);
    int invalidTotalRound = 0;

    // when & then
    assertThatThrownBy(() -> studyFactory.create(
            StudyType.OFFLINE,
            STUDY_TITLE,
            STUDY_DESCRIPTION,
            STUDY_DESCRIPTION_NOTION_LINK,
            STUDY_SEMESTER,
            invalidTotalRound,
            DAY_OF_WEEK,
            STUDY_START_TIME,
            STUDY_END_TIME,
            STUDY_APPLICATION_PERIOD,
            STUDY_DISCORD_CHANNEL_ID,
            STUDY_DISCORD_ROLE_ID,
            mentor,
            generator))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("총 회차는 1 이상이어야 합니다");
}

Also applies to: 51-79, 81-108


@Test
void 스터디_생성시_스터디회차는_순서대로_position이_지정되어_생성된다() {
// given
Member mentor = fixtureHelper.createMentor(1L);
AttendanceNumberGenerator generator = new FixedAttendanceNumberGenerator();
int totalRound = 8;

// when
StudyV2 study = studyFactory.create(
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,
mentor,
generator);

// then
assertThat(study.getStudySessions())
.extracting(StudySessionV2::getPosition)
.containsExactly(1, 2, 3, 4, 5, 6, 7, 8);
}

@Test
void 스터디_생성시_각_스터디회차에_출석번호가_생성된다() {
// given
Member mentor = fixtureHelper.createMentor(1L);
AttendanceNumberGenerator generator = new FixedAttendanceNumberGenerator();

// when
StudyV2 study = studyFactory.create(
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,
mentor,
generator);

// then
assertThat(study.getStudySessions())
.extracting(StudySessionV2::getLessonAttendanceNumber)
.containsOnly(1000);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.gdschongik.gdsc.global.common.constant;

import com.gdschongik.gdsc.domain.common.model.SemesterType;
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 java.time.DayOfWeek;
import java.time.LocalDateTime;
Expand All @@ -10,12 +12,20 @@ public class StudyConstant {
private StudyConstant() {}

public static final String STUDY_TITLE = "스터디 제목";
public static final String STUDY_DESCRIPTION = "스터디 설명";
public static final String STUDY_DESCRIPTION_NOTION_LINK = "https://gdschongik.com/2025-1-backend-study";
public static final Semester STUDY_SEMESTER = Semester.of(2025, SemesterType.FIRST);
public static final Long TOTAL_WEEK = 8L;
public static final int TOTAL_ROUND = 8;
public static final StudyType ONLINE_STUDY = StudyType.ONLINE;
public static final StudyType ASSIGNMENT_STUDY = StudyType.ASSIGNMENT;
public static final DayOfWeek DAY_OF_WEEK = DayOfWeek.FRIDAY;
public static final LocalTime STUDY_START_TIME = LocalTime.of(19, 0, 0);
public static final LocalTime STUDY_END_TIME = LocalTime.of(20, 0, 0);
public static final Period STUDY_APPLICATION_PERIOD =
Period.of(LocalDateTime.of(2025, 3, 1, 0, 0), LocalDateTime.of(2025, 3, 14, 23, 59));
public static final String STUDY_DISCORD_CHANNEL_ID = "12345678";
public static final String STUDY_DISCORD_ROLE_ID = "12345678";
Comment on lines +27 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Discord ID와 같은 민감한 정보는 환경 변수로 관리하는 것이 좋습니다.

Discord 채널 ID와 역할 ID를 상수로 하드코딩하는 것은 보안상 위험할 수 있습니다.

테스트용 값이라도 실제 ID 형식을 따르는 것이 좋으며, 프로덕션 환경에서는 환경 변수로 관리하는 것을 권장드립니다.


// StudyDetail
public static final String ATTENDANCE_NUMBER = "1234";
Expand Down