diff --git a/.env.example b/.env.example index 357f9af..9bcd8c1 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,8 @@ JWT_SECRET_KEY=replace-with-32-byte-or-longer-secret-key JWT_ISSUER=stream-server # Access Token 만료 시간 (ms) JWT_ACCESS_TOKEN_EXPIRY=3600000 + +### infrastructure:db — MySQL ### +DB_URL=jdbc:mysql://localhost:3306/stream?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 +DB_USERNAME=stream +DB_PASSWORD=replace-with-db-password diff --git a/AGENTS.md b/AGENTS.md index c21a2d4..1dda785 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ api/ └── app-api # 학생 앱 (STUDENT) core/ ├── common # ErrorCode/BusinessException, PrincipalProvider 등 공유 커널 -└── domain/{도메인} # {Domain}Service(공개) / {Domain}ServiceImpl(internal) +└── domain/{모듈} # domain/{도메인}/{domain|repository|service|service.impl}, impl만 비공개 gateway/ ├── auth # Spring Security, JWT └── logging # MDC 필터, access log @@ -32,7 +32,7 @@ infrastructure/ └── outbox # 아웃박스 릴레이 (폴러·재발행) ``` -도메인 간 경계는 Gradle(컴파일 타임)로, 도메인 내부 공개/`internal` 경계는 `verify()`(CI)로 강제한다. +도메인 간 경계는 Gradle(컴파일 타임)로, 도메인 내부 `service.impl` 경계는 ArchUnit 테스트(CI)로 강제한다. `service.impl` 안의 클래스는 모두 package-private으로 둔다. ## 컨벤션 문서 diff --git a/bootstrap/build.gradle.kts b/bootstrap/build.gradle.kts index 4e0a6c4..d4fbcc8 100644 --- a/bootstrap/build.gradle.kts +++ b/bootstrap/build.gradle.kts @@ -27,6 +27,4 @@ dependencies { testImplementation(libs.springModulithDocs) testImplementation(libs.archunitJunit5) testRuntimeOnly(libs.junitPlatformLauncher) - // infrastructure:db가 끌어오는 JPA 자동구성이 DataSource를 요구하므로 테스트 전용 인메모리 DB로 컨텍스트 로딩을 지원 - testRuntimeOnly(libs.h2) } diff --git a/bootstrap/src/main/resources/application.yaml b/bootstrap/src/main/resources/application.yaml index ca0f3a9..73cc461 100644 --- a/bootstrap/src/main/resources/application.yaml +++ b/bootstrap/src/main/resources/application.yaml @@ -4,3 +4,4 @@ spring: config: import: - classpath:application-gateway-auth.yml + - classpath:application-infrastructure-db.yml diff --git a/bootstrap/src/test/java/kr/ac/kookmin/stream/DomainImplAccessTests.java b/bootstrap/src/test/java/kr/ac/kookmin/stream/DomainImplAccessTests.java new file mode 100644 index 0000000..f0cca2f --- /dev/null +++ b/bootstrap/src/test/java/kr/ac/kookmin/stream/DomainImplAccessTests.java @@ -0,0 +1,44 @@ +package kr.ac.kookmin.stream; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.Dependency; +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.conditions.ArchConditions; + +/** + * 도메인 모듈은 OPEN 모듈이라 Modulith verify()가 service.impl을 가리지 않는다. + * 대신 여기서 impl 패키지의 외부 참조와 public 노출을 막는다. + */ +@AnalyzeClasses(packages = "kr.ac.kookmin.stream", importOptions = ImportOption.DoNotIncludeTests.class) +class DomainImplAccessTests { + + private static final String IMPL_PACKAGE = "..service.impl.."; + + @ArchTest + static final ArchRule implIsOnlyReferencedFromItsOwnPackage = classes() + .that().resideInAPackage(IMPL_PACKAGE) + .should(ArchConditions.onlyHaveDependentsWhere(originInSamePackageAsTarget())) + .because("service.impl 패키지는 같은 패키지 안에서만 참조한다"); + + @ArchTest + static final ArchRule implHasNoPublicClasses = noClasses() + .that().resideInAPackage(IMPL_PACKAGE) + .should().bePublic() + .because("service.impl 패키지의 클래스는 package-private으로 둔다"); + + private static DescribedPredicate originInSamePackageAsTarget() { + return new DescribedPredicate<>("origin is in the same package as target") { + @Override + public boolean test(Dependency dependency) { + return dependency.getOriginClass().getPackageName() + .equals(dependency.getTargetClass().getPackageName()); + } + }; + } +} diff --git a/bootstrap/src/test/java/kr/ac/kookmin/stream/StreamServerApplicationTests.java b/bootstrap/src/test/java/kr/ac/kookmin/stream/StreamServerApplicationTests.java deleted file mode 100644 index 2935634..0000000 --- a/bootstrap/src/test/java/kr/ac/kookmin/stream/StreamServerApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package kr.ac.kookmin.stream; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class StreamServerApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/core/domain/event/build.gradle.kts b/core/domain/event/build.gradle.kts index 030cedb..fc64395 100644 --- a/core/domain/event/build.gradle.kts +++ b/core/domain/event/build.gradle.kts @@ -11,6 +11,10 @@ dependencies { implementation(libs.springContext) implementation(libs.springTx) + // @ApplicationModule(type = OPEN) 선언용(package-info). 경계 검증은 bootstrap 테스트가 수행한다 + compileOnly(platform(libs.springModulithBom)) + compileOnly(libs.springModulithApi) + testImplementation(platform(libs.junitBom)) testImplementation(libs.junitJupiter) testRuntimeOnly(libs.junitPlatformLauncher) diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java new file mode 100644 index 0000000..7c22bb1 --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java @@ -0,0 +1,36 @@ +package kr.ac.kookmin.stream.event.domain.archive.domain; + +import java.time.LocalDate; +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Archive { + + private Long id; + private String title; + private String content; + private LocalDate startDate; + private LocalDate endDate; + private String location; + private String departmentName; + private List imageIds; + + public static Archive of( + Long id, + String title, + String content, + LocalDate startDate, + LocalDate endDate, + String location, + String departmentName, + List imageIds + ) { + return new Archive(id, title, content, startDate, endDate, location, departmentName, imageIds); + } +} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveRelatedLink.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveRelatedLink.java new file mode 100644 index 0000000..43231cc --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveRelatedLink.java @@ -0,0 +1,28 @@ +package kr.ac.kookmin.stream.event.domain.archive.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ArchiveRelatedLink { + + private Long id; + private Long archiveId; + private String title; + private String url; + private int displayOrder; + + public static ArchiveRelatedLink of( + Long id, + Long archiveId, + String title, + String url, + int displayOrder + ) { + return new ArchiveRelatedLink(id, archiveId, title, url, displayOrder); + } +} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/Event.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/Event.java new file mode 100644 index 0000000..9c7c6e8 --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/Event.java @@ -0,0 +1,51 @@ +package kr.ac.kookmin.stream.event.domain.event.domain; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Event { + + private Long id; + private String title; + private String description; + private String target; + private String place; + private LocalDateTime eventStartAt; + private LocalDateTime eventEndAt; + private LocalDateTime applyStartAt; + private LocalDateTime applyEndAt; + private RecruitType recruitType; + private List imageIds; + private int capacity; + private RecruitStatus recruitStatus; + private Long createdBy; + + public static Event of( + Long id, + String title, + String description, + String target, + String place, + LocalDateTime eventStartAt, + LocalDateTime eventEndAt, + LocalDateTime applyStartAt, + LocalDateTime applyEndAt, + RecruitType recruitType, + List imageIds, + int capacity, + RecruitStatus recruitStatus, + Long createdBy + ) { + return new Event( + id, title, description, target, place, eventStartAt, eventEndAt, applyStartAt, + applyEndAt, recruitType, imageIds, capacity, recruitStatus, createdBy + ); + } +} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/EventApplication.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/EventApplication.java new file mode 100644 index 0000000..6e56c65 --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/EventApplication.java @@ -0,0 +1,31 @@ +package kr.ac.kookmin.stream.event.domain.event.domain; + +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class EventApplication { + + private Long id; + private Long eventId; + private Long memberId; + private EventApplicationStatus status; + private LocalDateTime appliedAt; + private LocalDateTime canceledAt; + + public static EventApplication of( + Long id, + Long eventId, + Long memberId, + EventApplicationStatus status, + LocalDateTime appliedAt, + LocalDateTime canceledAt + ) { + return new EventApplication(id, eventId, memberId, status, appliedAt, canceledAt); + } +} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/EventApplicationAnswer.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/EventApplicationAnswer.java new file mode 100644 index 0000000..bb97c06 --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/EventApplicationAnswer.java @@ -0,0 +1,29 @@ +package kr.ac.kookmin.stream.event.domain.event.domain; + +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class EventApplicationAnswer { + + private Long id; + private Long eventApplicationId; + private Long eventQuestionId; + private String answerText; + private List selectedOptions; + + public static EventApplicationAnswer of( + Long id, + Long eventApplicationId, + Long eventQuestionId, + String answerText, + List selectedOptions + ) { + return new EventApplicationAnswer(id, eventApplicationId, eventQuestionId, answerText, selectedOptions); + } +} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/EventApplicationStatus.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/EventApplicationStatus.java new file mode 100644 index 0000000..373847e --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/EventApplicationStatus.java @@ -0,0 +1,6 @@ +package kr.ac.kookmin.stream.event.domain.event.domain; + +public enum EventApplicationStatus { + APPLIED, + CANCELED +} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/EventQuestion.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/EventQuestion.java new file mode 100644 index 0000000..1efc0f0 --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/EventQuestion.java @@ -0,0 +1,33 @@ +package kr.ac.kookmin.stream.event.domain.event.domain; + +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class EventQuestion { + + private Long id; + private Long eventId; + private String questionText; + private QuestionType questionType; + private boolean required; + private int displayOrder; + private List options; + + public static EventQuestion of( + Long id, + Long eventId, + String questionText, + QuestionType questionType, + boolean required, + int displayOrder, + List options + ) { + return new EventQuestion(id, eventId, questionText, questionType, required, displayOrder, options); + } +} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/QuestionType.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/QuestionType.java new file mode 100644 index 0000000..bb1f51f --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/QuestionType.java @@ -0,0 +1,8 @@ +package kr.ac.kookmin.stream.event.domain.event.domain; + +public enum QuestionType { + SHORT_TEXT, + LONG_TEXT, + SINGLE_CHOICE, + MULTIPLE_CHOICE +} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/RecruitStatus.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/RecruitStatus.java new file mode 100644 index 0000000..8ebb86d --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/RecruitStatus.java @@ -0,0 +1,7 @@ +package kr.ac.kookmin.stream.event.domain.event.domain; + +public enum RecruitStatus { + BEFORE_OPEN, + OPEN, + CLOSED +} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/RecruitType.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/RecruitType.java new file mode 100644 index 0000000..8dbc058 --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/domain/RecruitType.java @@ -0,0 +1,6 @@ +package kr.ac.kookmin.stream.event.domain.event.domain; + +public enum RecruitType { + FIRST_COME, + OPEN +} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/Locker.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/Locker.java new file mode 100644 index 0000000..83d9bd5 --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/Locker.java @@ -0,0 +1,30 @@ +package kr.ac.kookmin.stream.event.domain.locker.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Locker { + + private Long id; + private String lockerNumber; + private String section; + private int rowNo; + private int columnNo; + private LockerStatus status; + + public static Locker of( + Long id, + String lockerNumber, + String section, + int rowNo, + int columnNo, + LockerStatus status + ) { + return new Locker(id, lockerNumber, section, rowNo, columnNo, status); + } +} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerApplication.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerApplication.java new file mode 100644 index 0000000..6612d94 --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerApplication.java @@ -0,0 +1,21 @@ +package kr.ac.kookmin.stream.event.domain.locker.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class LockerApplication { + + private Long id; + private Long lockerPeriodId; + private Long memberId; + private Long lockerId; + + public static LockerApplication of(Long id, Long lockerPeriodId, Long memberId, Long lockerId) { + return new LockerApplication(id, lockerPeriodId, memberId, lockerId); + } +} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerPeriod.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerPeriod.java new file mode 100644 index 0000000..04e16cb --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerPeriod.java @@ -0,0 +1,32 @@ +package kr.ac.kookmin.stream.event.domain.locker.domain; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class LockerPeriod { + + private Long id; + private String name; + private LocalDateTime applyStartAt; + private LocalDateTime applyEndAt; + private LocalDate usageStartAt; + private LocalDate usageEndAt; + + public static LockerPeriod of( + Long id, + String name, + LocalDateTime applyStartAt, + LocalDateTime applyEndAt, + LocalDate usageStartAt, + LocalDate usageEndAt + ) { + return new LockerPeriod(id, name, applyStartAt, applyEndAt, usageStartAt, usageEndAt); + } +} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerStatus.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerStatus.java new file mode 100644 index 0000000..adba7ae --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerStatus.java @@ -0,0 +1,6 @@ +package kr.ac.kookmin.stream.event.domain.locker.domain; + +public enum LockerStatus { + AVAILABLE, + DISABLED +} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/package-info.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/package-info.java new file mode 100644 index 0000000..0f0411a --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/package-info.java @@ -0,0 +1,8 @@ +/** + * 계층 패키지(domain/{도메인}/{domain|repository|service})를 그대로 공개하기 위해 OPEN으로 둔다. + * service.impl 접근 차단은 bootstrap의 DomainImplAccessTests(ArchUnit)가 담당한다. + */ +@ApplicationModule(type = ApplicationModule.Type.OPEN) +package kr.ac.kookmin.stream.event; + +import org.springframework.modulith.ApplicationModule; diff --git a/core/domain/internal/build.gradle.kts b/core/domain/internal/build.gradle.kts index 12d3766..7703243 100644 --- a/core/domain/internal/build.gradle.kts +++ b/core/domain/internal/build.gradle.kts @@ -11,6 +11,10 @@ dependencies { implementation(libs.springContext) implementation(libs.springTx) + // @ApplicationModule(type = OPEN) 선언용(package-info). 경계 검증은 bootstrap 테스트가 수행한다 + compileOnly(platform(libs.springModulithBom)) + compileOnly(libs.springModulithApi) + testImplementation(platform(libs.junitBom)) testImplementation(libs.junitJupiter) testRuntimeOnly(libs.junitPlatformLauncher) diff --git a/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/config/domain/AdminConfigValue.java b/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/config/domain/AdminConfigValue.java new file mode 100644 index 0000000..bc861da --- /dev/null +++ b/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/config/domain/AdminConfigValue.java @@ -0,0 +1,20 @@ +package kr.ac.kookmin.stream.internal.domain.config.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class AdminConfigValue { + + private Long id; + private String configKey; + private String configValue; + + public static AdminConfigValue of(Long id, String configKey, String configValue) { + return new AdminConfigValue(id, configKey, configValue); + } +} diff --git a/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/display/domain/DisplayCalendarSchedule.java b/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/display/domain/DisplayCalendarSchedule.java new file mode 100644 index 0000000..37fb9d0 --- /dev/null +++ b/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/display/domain/DisplayCalendarSchedule.java @@ -0,0 +1,21 @@ +package kr.ac.kookmin.stream.internal.domain.display.domain; + +import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class DisplayCalendarSchedule { + + private Long id; + private LocalDate date; + private String schedules; + + public static DisplayCalendarSchedule of(Long id, LocalDate date, String schedules) { + return new DisplayCalendarSchedule(id, date, schedules); + } +} diff --git a/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/display/domain/DisplayPoster.java b/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/display/domain/DisplayPoster.java new file mode 100644 index 0000000..d73edf2 --- /dev/null +++ b/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/display/domain/DisplayPoster.java @@ -0,0 +1,21 @@ +package kr.ac.kookmin.stream.internal.domain.display.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class DisplayPoster { + + private Long id; + private String title; + private String imageKey; + private boolean active; + + public static DisplayPoster of(Long id, String title, String imageKey, boolean active) { + return new DisplayPoster(id, title, imageKey, active); + } +} diff --git a/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/file/domain/File.java b/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/file/domain/File.java new file mode 100644 index 0000000..e3bc2d2 --- /dev/null +++ b/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/file/domain/File.java @@ -0,0 +1,32 @@ +package kr.ac.kookmin.stream.internal.domain.file.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class File { + + private Long id; + private String fileKey; + private FileCategory category; + private String originalName; + private long fileSize; + private String contentType; + private Long uploaderId; + + public static File of( + Long id, + String fileKey, + FileCategory category, + String originalName, + long fileSize, + String contentType, + Long uploaderId + ) { + return new File(id, fileKey, category, originalName, fileSize, contentType, uploaderId); + } +} diff --git a/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/file/domain/FileCategory.java b/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/file/domain/FileCategory.java new file mode 100644 index 0000000..881b384 --- /dev/null +++ b/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/file/domain/FileCategory.java @@ -0,0 +1,9 @@ +package kr.ac.kookmin.stream.internal.domain.file.domain; + +public enum FileCategory { + TEMP, + NOTICE_ATTACHMENT, + NOTICE_IMAGE, + EVENT_IMAGE, + ARCHIVE_IMAGE +} diff --git a/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/schedule/domain/WorkSchedule.java b/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/schedule/domain/WorkSchedule.java new file mode 100644 index 0000000..e010c2d --- /dev/null +++ b/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/domain/schedule/domain/WorkSchedule.java @@ -0,0 +1,28 @@ +package kr.ac.kookmin.stream.internal.domain.schedule.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class WorkSchedule { + + private Long id; + private Long memberId; + private String dayOfWeek; + private int startHour; + private int endHour; + + public static WorkSchedule of( + Long id, + Long memberId, + String dayOfWeek, + int startHour, + int endHour + ) { + return new WorkSchedule(id, memberId, dayOfWeek, startHour, endHour); + } +} diff --git a/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/package-info.java b/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/package-info.java new file mode 100644 index 0000000..0f446ee --- /dev/null +++ b/core/domain/internal/src/main/java/kr/ac/kookmin/stream/internal/package-info.java @@ -0,0 +1,8 @@ +/** + * 계층 패키지(domain/{도메인}/{domain|repository|service})를 그대로 공개하기 위해 OPEN으로 둔다. + * service.impl 접근 차단은 bootstrap의 DomainImplAccessTests(ArchUnit)가 담당한다. + */ +@ApplicationModule(type = ApplicationModule.Type.OPEN) +package kr.ac.kookmin.stream.internal; + +import org.springframework.modulith.ApplicationModule; diff --git a/core/domain/member/build.gradle.kts b/core/domain/member/build.gradle.kts index d0cc140..0171f61 100644 --- a/core/domain/member/build.gradle.kts +++ b/core/domain/member/build.gradle.kts @@ -11,6 +11,10 @@ dependencies { implementation(libs.springContext) implementation(libs.springTx) + // @ApplicationModule(type = OPEN) 선언용(package-info). 경계 검증은 bootstrap 테스트가 수행한다 + compileOnly(platform(libs.springModulithBom)) + compileOnly(libs.springModulithApi) + testImplementation(platform(libs.junitBom)) testImplementation(libs.junitJupiter) testRuntimeOnly(libs.junitPlatformLauncher) diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/Member.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/Member.java deleted file mode 100644 index c6d54f9..0000000 --- a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/Member.java +++ /dev/null @@ -1,7 +0,0 @@ -package kr.ac.kookmin.stream.member; - -public record Member( - Long id, - String studentNo, - String name -) {} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/MemberRepository.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/MemberRepository.java deleted file mode 100644 index 3f98642..0000000 --- a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/MemberRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package kr.ac.kookmin.stream.member; - -import java.util.Optional; - -public interface MemberRepository { - Optional findById(Long id); -} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/MemberService.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/MemberService.java deleted file mode 100644 index e09a75e..0000000 --- a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/MemberService.java +++ /dev/null @@ -1,5 +0,0 @@ -package kr.ac.kookmin.stream.member; - -public interface MemberService { - Member getById(Long id); -} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Department.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Department.java new file mode 100644 index 0000000..c5b5c6a --- /dev/null +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Department.java @@ -0,0 +1,9 @@ +package kr.ac.kookmin.stream.member.domain.member.domain; + +/** + * 학부. 학생회 부서(CouncilDepartment)와는 다른 개념이다. + */ +public enum Department { + AI, + SW +} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Member.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Member.java new file mode 100644 index 0000000..d3a98d3 --- /dev/null +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Member.java @@ -0,0 +1,38 @@ +package kr.ac.kookmin.stream.member.domain.member.domain; + +import kr.ac.kookmin.stream.common.CouncilDepartment; +import kr.ac.kookmin.stream.common.Role; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Member { + + private Long id; + private String studentId; + private String name; + private Department department; + private String email; + private String fcmToken; + private boolean feePaid; + private Role role; + private CouncilDepartment councilDepartment; + + public static Member of( + Long id, + String studentId, + String name, + Department department, + String email, + String fcmToken, + boolean feePaid, + Role role, + CouncilDepartment councilDepartment + ) { + return new Member(id, studentId, name, department, email, fcmToken, feePaid, role, councilDepartment); + } +} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/MemberErrorCode.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberErrorCode.java similarity index 88% rename from core/domain/member/src/main/java/kr/ac/kookmin/stream/member/MemberErrorCode.java rename to core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberErrorCode.java index f546827..5df8fbb 100644 --- a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/MemberErrorCode.java +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberErrorCode.java @@ -1,4 +1,4 @@ -package kr.ac.kookmin.stream.member; +package kr.ac.kookmin.stream.member.domain.member.domain; import kr.ac.kookmin.stream.common.ErrorCode; import kr.ac.kookmin.stream.common.ErrorStatus; diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberTermAgreement.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberTermAgreement.java new file mode 100644 index 0000000..e330dc6 --- /dev/null +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/MemberTermAgreement.java @@ -0,0 +1,31 @@ +package kr.ac.kookmin.stream.member.domain.member.domain; + +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class MemberTermAgreement { + + private Long id; + private Long memberId; + private TermType termType; + private String termVersion; + private boolean agreed; + private LocalDateTime agreedAt; + + public static MemberTermAgreement of( + Long id, + Long memberId, + TermType termType, + String termVersion, + boolean agreed, + LocalDateTime agreedAt + ) { + return new MemberTermAgreement(id, memberId, termType, termVersion, agreed, agreedAt); + } +} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/TermType.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/TermType.java new file mode 100644 index 0000000..68758db --- /dev/null +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/TermType.java @@ -0,0 +1,6 @@ +package kr.ac.kookmin.stream.member.domain.member.domain; + +public enum TermType { + PRIVACY_POLICY, // 개인정보 이용 동의 + TERMS_OF_SERVICE // 서비스 이용 약관 동의 +} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/repository/MemberRepository.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/repository/MemberRepository.java new file mode 100644 index 0000000..259adc5 --- /dev/null +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/repository/MemberRepository.java @@ -0,0 +1,8 @@ +package kr.ac.kookmin.stream.member.domain.member.repository; + +import java.util.Optional; +import kr.ac.kookmin.stream.member.domain.member.domain.Member; + +public interface MemberRepository { + Optional findById(Long id); +} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/MemberService.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/MemberService.java new file mode 100644 index 0000000..203e04b --- /dev/null +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/MemberService.java @@ -0,0 +1,7 @@ +package kr.ac.kookmin.stream.member.domain.member.service; + +import kr.ac.kookmin.stream.member.domain.member.domain.Member; + +public interface MemberService { + Member getById(Long id); +} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/internal/MemberServiceImpl.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/impl/MemberServiceImpl.java similarity index 56% rename from core/domain/member/src/main/java/kr/ac/kookmin/stream/member/internal/MemberServiceImpl.java rename to core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/impl/MemberServiceImpl.java index 21ae572..3d34891 100644 --- a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/internal/MemberServiceImpl.java +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/service/impl/MemberServiceImpl.java @@ -1,10 +1,10 @@ -package kr.ac.kookmin.stream.member.internal; +package kr.ac.kookmin.stream.member.domain.member.service.impl; import kr.ac.kookmin.stream.common.BusinessException; -import kr.ac.kookmin.stream.member.Member; -import kr.ac.kookmin.stream.member.MemberErrorCode; -import kr.ac.kookmin.stream.member.MemberRepository; -import kr.ac.kookmin.stream.member.MemberService; +import kr.ac.kookmin.stream.member.domain.member.domain.Member; +import kr.ac.kookmin.stream.member.domain.member.domain.MemberErrorCode; +import kr.ac.kookmin.stream.member.domain.member.repository.MemberRepository; +import kr.ac.kookmin.stream.member.domain.member.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/notification/domain/MemberNotificationSetting.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/notification/domain/MemberNotificationSetting.java new file mode 100644 index 0000000..feaeefc --- /dev/null +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/notification/domain/MemberNotificationSetting.java @@ -0,0 +1,30 @@ +package kr.ac.kookmin.stream.member.domain.notification.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class MemberNotificationSetting { + + private Long id; + private Long memberId; + private boolean rentalEnabled; + private boolean eventEnabled; + private boolean lockerEnabled; + private boolean noticeEnabled; + + public static MemberNotificationSetting of( + Long id, + Long memberId, + boolean rentalEnabled, + boolean eventEnabled, + boolean lockerEnabled, + boolean noticeEnabled + ) { + return new MemberNotificationSetting(id, memberId, rentalEnabled, eventEnabled, lockerEnabled, noticeEnabled); + } +} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/notification/domain/Notification.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/notification/domain/Notification.java new file mode 100644 index 0000000..dff51e4 --- /dev/null +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/notification/domain/Notification.java @@ -0,0 +1,28 @@ +package kr.ac.kookmin.stream.member.domain.notification.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Notification { + + private Long id; + private Long memberId; + private String type; + private String formatValues; + private boolean read; + + public static Notification of( + Long id, + Long memberId, + String type, + String formatValues, + boolean read + ) { + return new Notification(id, memberId, type, formatValues, read); + } +} diff --git a/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/package-info.java b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/package-info.java new file mode 100644 index 0000000..5cf2f03 --- /dev/null +++ b/core/domain/member/src/main/java/kr/ac/kookmin/stream/member/package-info.java @@ -0,0 +1,8 @@ +/** + * 계층 패키지(domain/{도메인}/{domain|repository|service})를 그대로 공개하기 위해 OPEN으로 둔다. + * service.impl 접근 차단은 bootstrap의 DomainImplAccessTests(ArchUnit)가 담당한다. + */ +@ApplicationModule(type = ApplicationModule.Type.OPEN) +package kr.ac.kookmin.stream.member; + +import org.springframework.modulith.ApplicationModule; diff --git a/core/domain/welfare/build.gradle.kts b/core/domain/welfare/build.gradle.kts index f73084d..87dbaed 100644 --- a/core/domain/welfare/build.gradle.kts +++ b/core/domain/welfare/build.gradle.kts @@ -11,6 +11,10 @@ dependencies { implementation(libs.springContext) implementation(libs.springTx) + // @ApplicationModule(type = OPEN) 선언용(package-info). 경계 검증은 bootstrap 테스트가 수행한다 + compileOnly(platform(libs.springModulithBom)) + compileOnly(libs.springModulithApi) + testImplementation(platform(libs.junitBom)) testImplementation(libs.junitJupiter) testRuntimeOnly(libs.junitPlatformLauncher) diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/chat/domain/ChatMessage.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/chat/domain/ChatMessage.java new file mode 100644 index 0000000..12de339 --- /dev/null +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/chat/domain/ChatMessage.java @@ -0,0 +1,21 @@ +package kr.ac.kookmin.stream.welfare.domain.chat.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ChatMessage { + + private Long id; + private Long memberId; + private String senderType; + private String content; + + public static ChatMessage of(Long id, Long memberId, String senderType, String content) { + return new ChatMessage(id, memberId, senderType, content); + } +} diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/fee/domain/Payer.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/fee/domain/Payer.java new file mode 100644 index 0000000..357a40c --- /dev/null +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/fee/domain/Payer.java @@ -0,0 +1,30 @@ +package kr.ac.kookmin.stream.welfare.domain.fee.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Payer { + + private Long id; + private Long memberId; + private String name; + private String studentId; + private String enrollmentYear; + private boolean registered; + + public static Payer of( + Long id, + Long memberId, + String name, + String studentId, + String enrollmentYear, + boolean registered + ) { + return new Payer(id, memberId, name, studentId, enrollmentYear, registered); + } +} diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/fee/domain/PaymentStatus.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/fee/domain/PaymentStatus.java new file mode 100644 index 0000000..3ceb47b --- /dev/null +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/fee/domain/PaymentStatus.java @@ -0,0 +1,6 @@ +package kr.ac.kookmin.stream.welfare.domain.fee.domain; + +public enum PaymentStatus { + UNPAID, + PAID +} diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/fee/domain/StudentFee.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/fee/domain/StudentFee.java new file mode 100644 index 0000000..7855d73 --- /dev/null +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/fee/domain/StudentFee.java @@ -0,0 +1,33 @@ +package kr.ac.kookmin.stream.welfare.domain.fee.domain; + +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class StudentFee { + + private Long id; + private Long memberId; + private int amount; + private PaymentStatus paymentStatus; + private String paymentLinkUrl; + private LocalDateTime paidAt; + private Long confirmedBy; + + public static StudentFee of( + Long id, + Long memberId, + int amount, + PaymentStatus paymentStatus, + String paymentLinkUrl, + LocalDateTime paidAt, + Long confirmedBy + ) { + return new StudentFee(id, memberId, amount, paymentStatus, paymentLinkUrl, paidAt, confirmedBy); + } +} diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/feedback/domain/OpenFeedback.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/feedback/domain/OpenFeedback.java new file mode 100644 index 0000000..435c511 --- /dev/null +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/feedback/domain/OpenFeedback.java @@ -0,0 +1,35 @@ +package kr.ac.kookmin.stream.welfare.domain.feedback.domain; + +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class OpenFeedback { + + private Long id; + private int round; + private String category; + private String question; + private String answer; + private LocalDateTime answeredAt; + private Long createdBy; + private Long answeredBy; + + public static OpenFeedback of( + Long id, + int round, + String category, + String question, + String answer, + LocalDateTime answeredAt, + Long createdBy, + Long answeredBy + ) { + return new OpenFeedback(id, round, category, question, answer, answeredAt, createdBy, answeredBy); + } +} diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/Notice.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/Notice.java new file mode 100644 index 0000000..af2e2b2 --- /dev/null +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/Notice.java @@ -0,0 +1,35 @@ +package kr.ac.kookmin.stream.welfare.domain.notice.domain; + +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Notice { + + private Long id; + private String title; + private String content; + private NoticeCategory category; + private boolean pinned; + private Long createdBy; + private List attachmentIds; + private List imageIds; + + public static Notice of( + Long id, + String title, + String content, + NoticeCategory category, + boolean pinned, + Long createdBy, + List attachmentIds, + List imageIds + ) { + return new Notice(id, title, content, category, pinned, createdBy, attachmentIds, imageIds); + } +} diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeCategory.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeCategory.java new file mode 100644 index 0000000..e241a79 --- /dev/null +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeCategory.java @@ -0,0 +1,6 @@ +package kr.ac.kookmin.stream.welfare.domain.notice.domain; + +public enum NoticeCategory { + GENERAL, + PARTNERSHIP +} diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/domain/Item.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/domain/Item.java new file mode 100644 index 0000000..76b1861 --- /dev/null +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/domain/Item.java @@ -0,0 +1,22 @@ +package kr.ac.kookmin.stream.welfare.domain.rental.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Item { + + private Long id; + private String name; + private ItemType type; + private int count; + private String imageKey; + + public static Item of(Long id, String name, ItemType type, int count, String imageKey) { + return new Item(id, name, type, count, imageKey); + } +} diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/domain/ItemType.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/domain/ItemType.java new file mode 100644 index 0000000..08760e9 --- /dev/null +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/domain/ItemType.java @@ -0,0 +1,6 @@ +package kr.ac.kookmin.stream.welfare.domain.rental.domain; + +public enum ItemType { + CONSUMABLE, + RENTAL +} diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/domain/RentalHistory.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/domain/RentalHistory.java new file mode 100644 index 0000000..f135e33 --- /dev/null +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/domain/RentalHistory.java @@ -0,0 +1,42 @@ +package kr.ac.kookmin.stream.welfare.domain.rental.domain; + +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class RentalHistory { + + private Long id; + private Long itemId; + private Long memberId; + private Long workerId; + private String itemCode; + private RentalStatus rentalStatus; + private int rentedCount; + private LocalDateTime appliedAt; + private LocalDateTime rentAt; + private LocalDateTime returnedAt; + + public static RentalHistory of( + Long id, + Long itemId, + Long memberId, + Long workerId, + String itemCode, + RentalStatus rentalStatus, + int rentedCount, + LocalDateTime appliedAt, + LocalDateTime rentAt, + LocalDateTime returnedAt + ) { + return new RentalHistory( + id, itemId, memberId, workerId, itemCode, rentalStatus, rentedCount, appliedAt, rentAt, + returnedAt + ); + } +} diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/domain/RentalStatus.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/domain/RentalStatus.java new file mode 100644 index 0000000..0e9c4ed --- /dev/null +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/domain/RentalStatus.java @@ -0,0 +1,12 @@ +package kr.ac.kookmin.stream.welfare.domain.rental.domain; + +public enum RentalStatus { + PENDING, + CONFIRMED, + REJECTED, + CANCEL, + RENTAL, + RETURN_PENDING, + RETURNED, + RETURN_CONFIRMED +} diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/domain/RentalStatusWorkerLog.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/domain/RentalStatusWorkerLog.java new file mode 100644 index 0000000..0db7a2b --- /dev/null +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/domain/RentalStatusWorkerLog.java @@ -0,0 +1,26 @@ +package kr.ac.kookmin.stream.welfare.domain.rental.domain; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class RentalStatusWorkerLog { + + private Long id; + private Long rentalHistoryId; + private Long workerId; + private RentalStatus rentalStatus; + + public static RentalStatusWorkerLog of( + Long id, + Long rentalHistoryId, + Long workerId, + RentalStatus rentalStatus + ) { + return new RentalStatusWorkerLog(id, rentalHistoryId, workerId, rentalStatus); + } +} diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/package-info.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/package-info.java new file mode 100644 index 0000000..462f33c --- /dev/null +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/package-info.java @@ -0,0 +1,8 @@ +/** + * 계층 패키지(domain/{도메인}/{domain|repository|service})를 그대로 공개하기 위해 OPEN으로 둔다. + * service.impl 접근 차단은 bootstrap의 DomainImplAccessTests(ArchUnit)가 담당한다. + */ +@ApplicationModule(type = ApplicationModule.Type.OPEN) +package kr.ac.kookmin.stream.welfare; + +import org.springframework.modulith.ApplicationModule; diff --git a/docs/conventions/00-index.md b/docs/conventions/00-index.md index b8bcedd..fd4011e 100644 --- a/docs/conventions/00-index.md +++ b/docs/conventions/00-index.md @@ -5,7 +5,7 @@ Java 21 + Spring Boot 4.1 + Spring Modulith 기반, 단일 학생회 플랫폼 **핵심 전제** - 학생이 **대여·행사 신청·사물함 신청·회비 납부** 등을, 운영진(부서별 관리자)이 관리를 수행하는 **단일 학생회** 플랫폼(테넌트 식별자 없음). - 언어 Java 21, DB MySQL 8.x. Spring Modulith는 경계 검증 `verify()` 전용. -- 도메인 간 경계는 Gradle(컴파일 타임), 도메인 내부(공개/`internal`) 경계는 `verify()`(CI 타임)로 강제. +- 도메인 간 경계는 Gradle(컴파일 타임), 도메인 내부(`service.impl`) 경계는 ArchUnit 테스트(CI 타임)로 강제. Modulith `verify()`는 모듈 간 순환·의존 검사. - 교차 도메인 동기 조합은 api의 `UseCase`, 비동기 반응은 이벤트 + 직접 구현 아웃박스. - 권한은 2계층: 기본 role(`STUDENT`/`ADMIN`) + 관리자 부서(`Department`, `DEPT_*`). @@ -30,7 +30,7 @@ Java 21 + Spring Boot 4.1 + Spring Modulith 기반, 단일 학생회 플랫폼 - "soft delete 컬럼 인덱스/유니크" → `flyway-migration.md` 3-4절 - "객체를 어떻게 생성하지 / Lombok 어디까지" → `coding-style.md` 2-10·2-11절 - "에러 코드 추가" → `error-handling.md` -- "도메인 내부 구현 숨기기" → `architecture.md` 4-3절 (최상위 공개 / `internal`) +- "도메인 내부 구현 숨기기 / 패키지 구조" → `architecture.md` 4-3절 (`domain/{도메인}/{domain|repository|service|service.impl}`, OPEN 모듈 + ArchUnit) ## 모듈 한눈에 보기 @@ -44,8 +44,8 @@ core/ ├── common # ErrorCode/BusinessException/ErrorStatus, PrincipalProvider/Role/Department, │ # PageResult/CursorSliceResult, OutboxWriter, common.event (shared module) └── domain/{auth,member,event,welfare,internal} - # {Domain}Service(공개)/{Domain}ServiceImpl(internal)/ - # {Domain}Repository·{Domain}Client(공개)/{Domain}ErrorCode + # 모듈 안은 domain/{도메인}/{domain|repository|service|service.impl} + # OPEN 모듈. service.impl만 비공개(package-private, ArchUnit이 검사) gateway/ ├── auth # Spring Security, JWT, DepartmentAccessChecker └── logging # MDC 필터, access log diff --git a/docs/conventions/architecture.md b/docs/conventions/architecture.md index edb96d7..59dba10 100644 --- a/docs/conventions/architecture.md +++ b/docs/conventions/architecture.md @@ -21,7 +21,7 @@ - Spring Boot 4.1.0은 최소 Java 17을 요구하며 Java 26까지 호환되고 Spring Framework 7.0.8 이상을 필요로 한다. Java 21은 가상 스레드 등 JVM 기능을 위해 권장되는 버전이다. - 버전은 루트 `gradle/libs.versions.toml`(버전 카탈로그)에서 단일 관리한다. 모듈별 `build.gradle.kts`에 버전을 하드코딩하지 않는다. - Spring Boot 플러그인과 `bootJar`는 **`bootstrap` 모듈에서만** 활성화한다. 나머지는 라이브러리로 두고 Boot BOM을 `platform`으로 가져온다. -- Spring Modulith는 경계 검증(`verify()`) 목적으로만 쓰며, `spring-modulith-core`는 **`bootstrap`의 test scope에만** 둔다. 이벤트 처리에는 Modulith를 쓰지 않는다(6-2절). +- Spring Modulith는 경계 검증(`verify()`) 목적으로만 쓰며, `spring-modulith-core`는 **`bootstrap`의 test scope에만** 둔다. 도메인 모듈은 `@ApplicationModule(type = OPEN)` 선언을 위해 어노테이션 전용 `spring-modulith-api`만 `compileOnly`로 갖는다(4-3절). 이벤트 처리에는 Modulith를 쓰지 않는다(6-2절). --- @@ -155,7 +155,7 @@ core:common → (프로젝트 내 의존 없음) ``` - `core:domain:{도메인}`은 `core:common`에만 의존한다. 다른 도메인·web·security·JPA·Modulith에는 의존하지 않는다. Spring은 DI용 `spring-context`, 트랜잭션·이벤트 리스너용 `spring-tx`만 쓴다. -- `infrastructure:*`는 각 도메인의 공개 인터페이스를 구현하므로 해당 `core:domain:{도메인}`에 의존하되(단 `outbox` 제외), 도메인 `internal`에는 접근하지 않는다. +- `infrastructure:*`는 각 도메인의 공개 인터페이스를 구현하므로 해당 `core:domain:{도메인}`에 의존하되(단 `outbox` 제외), 도메인의 `service.impl`에는 접근하지 않는다. - `core:common`은 어떤 도메인 개념도 담지 않는다(`{Domain}Id` 같은 값 타입 금지). 순수 기술 어휘 + 인증 추상(PrincipalProvider·Role·Department) + 크로스 도메인 이벤트 타입 + 아웃박스 포트만 둔다. - 모듈 간 의존은 원칙적으로 `implementation`을 쓴다. `api`로 노출하면 전이 의존으로 레이어가 오염된다. @@ -168,8 +168,8 @@ Gradle 모듈 분리가 도메인 간 경계를 컴파일 타임에 막고, Spri ### 4-1. 두 경계의 강제 성격 - **도메인 간 경계(`{도메인A}` ↛ `{도메인B}`)**: Gradle이 컴파일 타임에 하드 강제한다. 의존을 안 걸면 상대 도메인 클래스는 컴파일 자체가 안 된다. -- **도메인 내부 경계(공개 / `internal`)**: Modulith `verify()`가 **CI 타임에** 강제한다. 같은 모듈 안이라 Gradle이 못 막고, 다른 모듈에서 `internal` 타입을 import해도 컴파일은 된다 — `verify()`가 빌드를 깨는 것이 유일한 방어선이다. -- 따라서 **`verify()` 테스트를 `gradle check`와 PR 머지 게이트에 필수로 건다.** 실행하지 않으면 내부 경계는 강제되지 않는다. +- **도메인 내부 경계(공개 계층 / `service.impl`)**: 도메인 모듈은 OPEN이라 Modulith가 가리지 않는다. `bootstrap`의 **ArchUnit 테스트(`DomainImplAccessTests`)** 가 CI 타임에 강제한다. 같은 모듈 안이라 Gradle이 못 막고, `impl`에 public 타입이 생기면 다른 모듈에서 import해도 컴파일은 된다 — ArchUnit이 빌드를 깨는 것이 방어선이다. +- 따라서 **`verify()`와 `DomainImplAccessTests`를 `gradle check`와 PR 머지 게이트에 필수로 건다.** 실행하지 않으면 내부 경계는 강제되지 않는다. ### 4-2. 패키지 컨벤션 (탐지 규칙) @@ -184,29 +184,52 @@ Gradle 모듈 분리가 도메인 간 경계를 컴파일 타임에 막고, Spri - `core:common`은 verify 설정에서 shared module로 선언해, 어디서든 의존해도 위반이 나지 않게 한다(어노테이션 없이 순수 Java 유지). - 도메인이 하나일 때부터 **일부러 위반을 만들어 `verify()`가 실패하는지** 확인한다. -### 4-3. 도메인 모듈의 공개 경계 +### 4-3. 도메인 모듈의 패키지 구조 & 공개 경계 -공개 타입을 모듈 **최상위 패키지**에, 내부 구현을 **`internal` 하위 패키지**에 둔다. Modulith가 최상위를 공개 API로, 하위 패키지를 내부로 간주한다. (`api`/`spi` 같은 named interface 패키지는 두지 않는다.) +Gradle 모듈 `core:domain:{모듈}` 하나가 Modulith 애플리케이션 모듈 하나(`{basePackage}.{모듈}`)다. 모듈 안은 **`domain/{도메인}`** 으로 도메인(aggregate) 단위를 나누고, 각 도메인 아래를 **계층 패키지**(`domain`/`repository`/`service`/`service.impl`)로 나눈다. 도메인 객체만 있는 도메인도 계층 패키지를 생략하지 않는다. -member 도메인 예시: +Modulith는 모듈 기본 패키지만 공개로, 모든 하위 패키지를 비공개로 보는데 이 구조와 맞지 않는다(공개 계층 패키지마다 `@NamedInterface`를 달아야 함). 그래서 **모듈 기본 패키지의 `package-info.java`에 `@ApplicationModule(type = OPEN)`을 선언해 하위 패키지를 모두 열고**, `service.impl` 차단은 `bootstrap`의 **ArchUnit 테스트(`DomainImplAccessTests`)** 가 담당한다(4-4절). +member 모듈 예시 (도메인 `member`·`notification`): + +``` +{basePackage}.member // Modulith 애플리케이션 모듈 (= core:domain:member) +├── package-info.java // @ApplicationModule(type = OPEN) +└── domain/ + ├── member/ // 도메인(aggregate) 단위 + │ ├── domain/ + │ │ ├── Member, Department, TermType, MemberTermAgreement // 도메인 객체·enum + │ │ └── MemberErrorCode // 도메인 에러 코드 (api Swagger 문서화가 참조) + │ ├── repository/ + │ │ └── MemberRepository // 아웃바운드 포트 (infrastructure:db가 구현) + │ └── service/ + │ ├── MemberService // 공개 진입점 + │ └── impl/ // 비공개 — 모든 클래스 package-private + │ └── MemberServiceImpl // 구현체, 그 외 내부 협력 객체 + └── notification/ + └── domain/ + └── Notification, MemberNotificationSetting ``` -{basePackage}.member -├── MemberService // 공개 인터페이스 (진입점) -├── Member, MemberRegisterCommand // 경계를 넘는 도메인 객체 / Command (record) -├── MemberRepository // 공개 인터페이스 (아웃바운드 포트, infrastructure:db가 구현) -├── MemberErrorCode // 도메인 에러 코드 (api의 Swagger 문서화가 참조 → 공개) -└── internal/ - └── MemberServiceImpl // MemberService 구현체 (감춰짐), 그 외 내부 협력 객체 + +```java +// {basePackage}/member/package-info.java +@ApplicationModule(type = ApplicationModule.Type.OPEN) +package kr.ac.kookmin.stream.member; + +import org.springframework.modulith.ApplicationModule; ``` -- 다른 모듈은 최상위 공개 타입만 참조한다. `internal` 외부 참조는 `verify()`가 차단한다. -- **Repository/Client 인터페이스는 `infrastructure:*`가 구현해야 하므로 `internal`이 아니라 공개(최상위)에 둔다.** 진입점(`{Domain}Service`)과 아웃바운드 포트(`{Domain}Repository`/`{Domain}Client`)는 공개, 그 구현·로직은 비공개. +- `service.impl` 밖의 클래스는 `service.impl` 안의 타입을 참조하지 않는다. 같은 도메인의 `service` 패키지도, 다른 도메인의 `impl`도 예외가 아니다 — 허용되는 참조는 **같은 `impl` 패키지 안**뿐이다(ArchUnit 규칙 1). +- `service.impl` 안의 모든 클래스는 **package-private**으로 선언한다(ArchUnit 규칙 2). 그래서 다른 모듈에서는 컴파일 자체가 안 되고, 협력 객체도 같은 패키지 안에서만 쓰인다. +- **Repository/Client 인터페이스는 `infrastructure:*`가 구현해야 하므로 `repository` 패키지(공개)에 둔다.** 진입점(`{Domain}Service`)과 아웃바운드 포트(`{Domain}Repository`/`{Domain}Client`)는 공개, 그 구현·로직은 `service.impl`에 비공개. - 소비 측은 공개 인터페이스를 주입받고, Spring이 런타임에 `{Domain}ServiceImpl`를 연결한다. -- 도메인 `internal` 내부 패키지 구조는 각 도메인이 자유롭게 설계한다. +- `service.impl` 내부 구조는 각 도메인이 자유롭게 설계한다. +- 새 모듈을 만들면 기본 패키지에 위 `package-info.java`를 함께 만든다. 도메인·계층 패키지를 추가할 때는 아무 선언도 필요 없다. ### 4-4. 검증 테스트 +두 테스트가 한 쌍이다. `ModularityTests.verify()`는 모듈 간 순환·의존 규칙을, `DomainImplAccessTests`는 도메인 모듈 안의 `service.impl` 경계를 검사한다. + ```java // bootstrap/src/test/.../ModularityTests.java class ModularityTests { @@ -225,6 +248,25 @@ class ModularityTests { } ``` +```java +// bootstrap/src/test/.../DomainImplAccessTests.java (ArchUnit) +@AnalyzeClasses(packages = "{basePackage}", importOptions = ImportOption.DoNotIncludeTests.class) +class DomainImplAccessTests { + + // 규칙 1 — service.impl은 같은 패키지 안에서만 참조한다 + @ArchTest + static final ArchRule implIsOnlyReferencedFromItsOwnPackage = classes() + .that().resideInAPackage("..service.impl..") + .should(ArchConditions.onlyHaveDependentsWhere(originInSamePackageAsTarget())); + + // 규칙 2 — service.impl의 클래스는 package-private으로 둔다 + @ArchTest + static final ArchRule implHasNoPublicClasses = noClasses() + .that().resideInAPackage("..service.impl..") + .should().bePublic(); +} +``` + --- ## 5. 레이어 구조 & 규칙 @@ -234,10 +276,10 @@ class ModularityTests { ``` Presentation Controller, Request/Response, UseCase → api:* 모듈 ↓ -Business {Domain}Service (공개 인터페이스) → core:domain:{도메인} (최상위) - {Domain}ServiceImpl (구현체) → core:domain:{도메인} (internal) +Business {Domain}Service (공개 인터페이스) → core:domain:{모듈} domain/{도메인}/service + {Domain}ServiceImpl (구현체) → core:domain:{모듈} domain/{도메인}/service/impl (비공개) ↓ -Data Access {Domain}Repository / {Domain}Client 인터페이스 → core:domain:{도메인} (최상위) +Data Access {Domain}Repository / {Domain}Client 인터페이스 → core:domain:{모듈} domain/{도메인}/repository └ 구현: {Domain}RepositoryImpl / {Domain}JpaRepository → infrastructure:db └ 구현: {Domain}ClientImpl → infrastructure:client ``` @@ -250,7 +292,7 @@ Data Access {Domain}Repository / {Domain}Client 인터페이스 → core:dom 4. 동일 레이어 간 참조를 금지한다. - **Business가 Data Access를 직접 참조한다** — `{Domain}ServiceImpl`가 `{Domain}Repository`를 직접 사용한다. -- 비즈니스 규칙 검증 등은 `{Domain}ServiceImpl` 또는 `internal` 패키지의 별도 협력 객체에 둔다. +- 비즈니스 규칙 검증 등은 `{Domain}ServiceImpl` 또는 `service.impl` 패키지의 별도 협력 객체에 둔다. - 도메인 객체는 `core:domain`에 `record`(불변)로 두되 JPA 어노테이션을 갖지 않는다. JPA Entity는 `infrastructure:db`에 둔다. --- @@ -264,7 +306,7 @@ Data Access {Domain}Repository / {Domain}Client 인터페이스 → core:dom 교차 도메인 조회·쓰기 흐름은 presentation의 `{Feature}UseCase`가 여러 도메인의 공개 `{Domain}Service`를 조합해 처리한다. - **`api:*` 모듈에 둔다.** `core`에는 두지 않는다. -- 공개 `{Domain}Service`만 조합한다. `{Domain}Repository`·`internal`에는 접근하지 않는다. +- 공개 `{Domain}Service`만 조합한다. `{Domain}Repository`·`service.impl`에는 접근하지 않는다. - Controller는 UseCase 하나만 참조한다: `Controller → UseCase → 각 도메인 Service`. - 서로 다른 도메인의 Service 2개 이상을 조합할 때만 UseCase를 만든다. **단일 도메인 흐름은 Controller가 그 `{Domain}Service`를 직접 참조**한다. - **트랜잭션은 원자성이 필요한 흐름에만 건다.** 유료 행사 신청(행사 정원 차감 + 회비/결제 반영)처럼 전부 성공/전부 실패해야 하는 경우에만 UseCase 메서드에 `@Transactional`을 선언한다(동일 DataSource 기준 한 트랜잭션). 운영진 대시보드 같은 조회 조합에는 트랜잭션을 걸지 않는다. @@ -304,7 +346,7 @@ Data Access {Domain}Repository / {Domain}Client 인터페이스 → core:dom | `bootstrap` | 모든 `api:*` + `gateway:*` + `infrastructure:*` + `core:common` + Spring Boot 실행(Actuator, 스케줄링) + `spring-modulith-core`·ArchUnit(**test scope**) | | `api:common-api` | `core:domain:{도메인}` + `core:common` + `gateway:auth`(DepartmentAccessChecker) + `gateway:logging` + Spring MVC + validation | | `api:{client}-api` | `api:common-api` + `core:domain:{도메인}` + `core:common` + `gateway:auth` + `gateway:logging` + Spring MVC + validation | -| `core:domain:{도메인}` | `core:common` + `spring-context`(DI) + `spring-tx` + 순수 Java. **Modulith·web·security·JPA 없음** | +| `core:domain:{도메인}` | `core:common` + `spring-context`(DI) + `spring-tx` + `spring-modulith-api`(**compileOnly**, `@ApplicationModule` 선언용) + 순수 Java. **Modulith core·web·security·JPA 없음** | | `core:common` | 순수 Java / 유틸리티만. Spring·Modulith·web·security·JPA 없음 | | `gateway:auth` | `core:common` + Spring Security + `jjwt` + `spring-webmvc`(예외 위임) | | `gateway:logging` | `core:common` + `spring-web` + `spring-context` + slf4j 등 + Servlet API | @@ -316,7 +358,7 @@ Data Access {Domain}Repository / {Domain}Client 인터페이스 → core:dom ## 8. 확장 · 미결 -- **도메인 `internal` 패키지 구조**: 각 도메인이 자유롭게 설계한다(4-3절 제약만 만족). +- **도메인 `service.impl` 패키지 구조**: 각 도메인이 자유롭게 설계한다(4-3절 제약만 만족). - **`common.event` 분가**: 크로스 도메인 이벤트가 늘면 `core:event-contract`로 분리. - **아웃박스 외부화**: 브로커(Kafka 등) 필요 시 릴레이 재발행 대상을 확장. 현재는 인프로세스 재발행만. - **레거시 role 흡수**: 기존 `USER/ADMIN/WORKER/GA` 시스템의 데이터 흡수·마이그레이션은 `role-migration-plan.md`에 계획으로만 정리(미구현). diff --git a/docs/conventions/coding-style.md b/docs/conventions/coding-style.md index fe8d892..ccc5d9b 100644 --- a/docs/conventions/coding-style.md +++ b/docs/conventions/coding-style.md @@ -8,7 +8,7 @@ ## 1. 네이밍 컨벤션 -Business는 공개 인터페이스 + `internal` 구현체로 구성한다. +Business는 공개 인터페이스 + `service.impl` 구현체로 구성한다. 도메인 모듈 안의 패키지는 `domain/{도메인}/{domain|repository|service|service.impl}`로 나누며, `service.impl` 안의 클래스는 모두 package-private으로 둔다(`architecture.md` 4-3절). | 레이어 | 역할 | 네이밍 | 위치 | | --- | --- | --- | --- | @@ -16,12 +16,12 @@ Business는 공개 인터페이스 + `internal` 구현체로 구성한다. | Presentation | 요청 객체 | `{Domain}{Action}Request` | `api:{client}-api` | | Presentation | 응답 객체 | `{Domain}{Action}Response` | `api:{client}-api` | | Presentation | 교차 도메인 조합 | `{Feature}UseCase` | `api:{client}-api` | -| Business | 공개 진입점 인터페이스 | `{Domain}Service` | `core:domain:{도메인}` (최상위) | -| Business | 구현체 | `{Domain}ServiceImpl` | `core:domain:{도메인}` (`internal`) | -| Data Access | Repository 인터페이스 | `{Domain}Repository` | `core:domain:{도메인}` (최상위, 공개) | +| Business | 공개 진입점 인터페이스 | `{Domain}Service` | `core:domain:{모듈}` `domain/{도메인}/service` (공개) | +| Business | 구현체 | `{Domain}ServiceImpl` | `core:domain:{모듈}` `domain/{도메인}/service/impl` (비공개) | +| Data Access | Repository 인터페이스 | `{Domain}Repository` | `core:domain:{모듈}` `domain/{도메인}/repository` (공개) | | Data Access | Repository 구현체 | `{Domain}RepositoryImpl` | `infrastructure:db` | | Data Access | JPA Repository | `{Domain}JpaRepository` | `infrastructure:db` | -| Data Access | 외부 API 클라이언트 인터페이스 | `{Domain}Client` | `core:domain:{도메인}` (최상위, 공개) | +| Data Access | 외부 API 클라이언트 인터페이스 | `{Domain}Client` | `core:domain:{모듈}` `domain/{도메인}/repository` (공개) | | Data Access | Client 구현체 | `{Domain}ClientImpl` | `infrastructure:client` | --- @@ -186,7 +186,7 @@ private MemberProfile profile; // record VO를 JSON 컬럼으로 - `core:domain`에 인터페이스만 공개로 선언하고, 부재는 `Optional`로 표현한다. ```java -// core:domain:member (공개) +// core:domain:member — domain/member/repository (공개) public interface MemberRepository { Optional findById(Long id); boolean existsByStudentNo(String studentNo); @@ -225,13 +225,13 @@ public class MemberRepositoryImpl implements MemberRepository { ### 2-7. Business Layer (Service) -- `{Domain}Service`는 공개 인터페이스(최상위), `{Domain}ServiceImpl`는 구현체(`internal`). 외부에 숨기기 위해 **package-private 클래스**로 선언한다(package-private `@Service`도 빈 등록됨). +- `{Domain}Service`는 공개 인터페이스(`service`), `{Domain}ServiceImpl`는 구현체(`service.impl`). `service.impl` 안의 클래스는 구현체·협력 객체 모두 **package-private**으로 선언한다(package-private `@Service`도 빈 등록됨). ArchUnit(`DomainImplAccessTests`)이 public 클래스와 외부 참조를 잡는다. - **`{Domain}ServiceImpl`가 `{Domain}Repository`를 직접 참조**한다. -- 비즈니스 규칙 검증은 `{Domain}ServiceImpl`(또는 internal 협력 객체)에서 하고 `BusinessException`을 던진다. +- 비즈니스 규칙 검증은 `{Domain}ServiceImpl`(또는 `service.impl` 협력 객체)에서 하고 `BusinessException`을 던진다. - 트랜잭션 경계는 Service 메서드에. 조회 전용은 `@Transactional(readOnly = true)`, 교차 도메인 UseCase가 감쌀 수 있게 **기본 전파(REQUIRED)** 를 쓴다(`architecture.md` 6-1절). ```java -// core:domain:member (최상위) +// core:domain:member — domain/member/service (공개) public interface MemberService { Member register(MemberRegisterCommand command); Member getById(Long id); @@ -239,7 +239,7 @@ public interface MemberService { ``` ```java -// core:domain:member/internal (감춰짐) +// core:domain:member — domain/member/service/impl (비공개) @Service @RequiredArgsConstructor class MemberServiceImpl implements MemberService { diff --git a/docs/conventions/error-handling.md b/docs/conventions/error-handling.md index 5ddd745..ab387f9 100644 --- a/docs/conventions/error-handling.md +++ b/docs/conventions/error-handling.md @@ -36,7 +36,7 @@ public final class ErrorStatus { ## 2. CommonErrorCode & 도메인 ErrorCode - 공통 에러 코드는 `core:common`의 `CommonErrorCode`. -- 도메인 에러 코드는 각 `core:domain:{도메인}`에 별도 enum으로 두고, api Swagger 문서화가 참조하므로 **공개(최상위)** 에 둔다. +- 도메인 에러 코드는 각 도메인의 `domain/{도메인}/domain` 패키지에 별도 enum으로 두고, api Swagger 문서화가 참조하므로 공개 패키지(`domain`)에 둔다. ```java // core:common — CommonErrorCode.java diff --git a/docs/conventions/git-convention.md b/docs/conventions/git-convention.md index 3e5a6f4..b87614e 100644 --- a/docs/conventions/git-convention.md +++ b/docs/conventions/git-convention.md @@ -124,7 +124,7 @@ Squash Merge 시 커밋 메시지에 `- #PR번호`를 추가한다. - `main`에 직접 push하지 않는다. - PR은 최소 1명 리뷰 승인 후 Merge한다. -- **CI 필수 체크**: 모듈 경계 검증(`ModularityTests.verify()`)을 포함한 `gradle check`가 통과해야 Merge할 수 있다(`architecture.md` 4-1절). `internal` 경계는 이 게이트로만 강제된다. +- **CI 필수 체크**: 모듈 경계 검증(`ModularityTests.verify()`)과 `service.impl` 경계 검증(`DomainImplAccessTests`, ArchUnit)을 포함한 `gradle check`가 통과해야 Merge할 수 있다(`architecture.md` 4-1절). 도메인 내부 경계는 이 게이트로만 강제된다. - Merge 후 작업 브랜치는 삭제한다. --- diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e30ebbe..968f0d2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,7 @@ springBootStarterSecurityTest = { module = "org.springframework.boot:spring-boot springBootStarterDataJpaTest = { module = "org.springframework.boot:spring-boot-starter-data-jpa-test" } # Spring Modulith — verify() boundary checks only, bootstrap test scope +springModulithApi = { module = "org.springframework.modulith:spring-modulith-api" } springModulithStarterTest = { module = "org.springframework.modulith:spring-modulith-starter-test" } springModulithDocs = { module = "org.springframework.modulith:spring-modulith-docs" } @@ -38,7 +39,6 @@ jacksonDatabind = { module = "com.fasterxml.jackson.core:jackson-databind" } flywayMysql = { module = "org.flywaydb:flyway-mysql" } mysqlConnectorJ = { module = "com.mysql:mysql-connector-j" } -h2 = { module = "com.h2database:h2" } jjwtApi = { module = "io.jsonwebtoken:jjwt-api", version.ref = "jjwtVersion" } jjwtImpl = { module = "io.jsonwebtoken:jjwt-impl", version.ref = "jjwtVersion" } diff --git a/infrastructure/db/build.gradle.kts b/infrastructure/db/build.gradle.kts index 9952581..903dc34 100644 --- a/infrastructure/db/build.gradle.kts +++ b/infrastructure/db/build.gradle.kts @@ -7,6 +7,9 @@ description = "JPA Entity, RepositoryImpl, Flyway 마이그레이션 (MySQL)" dependencies { implementation(project(":core:common")) implementation(project(":core:domain:member")) + implementation(project(":core:domain:event")) + implementation(project(":core:domain:welfare")) + implementation(project(":core:domain:internal")) implementation(platform(libs.springBootDependenciesBom)) implementation(libs.springBootStarterDataJpa) diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/common/BaseCreatedTimeEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/common/BaseCreatedTimeEntity.java new file mode 100644 index 0000000..d3ae193 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/common/BaseCreatedTimeEntity.java @@ -0,0 +1,14 @@ +package kr.ac.kookmin.stream.db.common; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; +import lombok.Getter; + +@Getter +@MappedSuperclass +public abstract class BaseCreatedTimeEntity { + + @Column(name = "created_at", insertable = false, updatable = false) + private LocalDateTime createdAt; +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/common/BaseSoftDeleteEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/common/BaseSoftDeleteEntity.java index 0b4cb9b..6235a04 100644 --- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/common/BaseSoftDeleteEntity.java +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/common/BaseSoftDeleteEntity.java @@ -2,17 +2,16 @@ import jakarta.persistence.Column; import jakarta.persistence.MappedSuperclass; -import java.time.LocalDateTime; import lombok.Getter; @Getter @MappedSuperclass public abstract class BaseSoftDeleteEntity extends BaseTimeEntity { - @Column(name = "deleted_at") - private LocalDateTime deletedAt; + @Column(name = "is_deleted", nullable = false) + private boolean isDeleted = false; public void delete() { - this.deletedAt = LocalDateTime.now(); + this.isDeleted = true; } } diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveJpaEntity.java new file mode 100644 index 0000000..824215f --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveJpaEntity.java @@ -0,0 +1,67 @@ +package kr.ac.kookmin.stream.db.event; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDate; +import java.util.List; +import kr.ac.kookmin.stream.db.common.BaseTimeEntity; +import kr.ac.kookmin.stream.event.domain.archive.domain.Archive; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +@Entity +@Table(name = "archives") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ArchiveJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "archive_id") + private Long id; + + @Column(nullable = false, length = 200) + private String title; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; + + @Column(name = "start_date", nullable = false) + private LocalDate startDate; + + @Column(name = "end_date") + private LocalDate endDate; + + private String location; + + @Column(name = "department_name", nullable = false, length = 100) + private String departmentName; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "image_ids", columnDefinition = "json") + private List imageIds; + + private ArchiveJpaEntity(Archive archive) { + this.id = archive.getId(); + this.title = archive.getTitle(); + this.content = archive.getContent(); + this.startDate = archive.getStartDate(); + this.endDate = archive.getEndDate(); + this.location = archive.getLocation(); + this.departmentName = archive.getDepartmentName(); + this.imageIds = archive.getImageIds(); + } + + public static ArchiveJpaEntity from(Archive archive) { + return new ArchiveJpaEntity(archive); + } + + public Archive toDomain() { + return Archive.of(id, title, content, startDate, endDate, location, departmentName, imageIds); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveRelatedLinkJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveRelatedLinkJpaEntity.java new file mode 100644 index 0000000..cd18c7c --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveRelatedLinkJpaEntity.java @@ -0,0 +1,57 @@ +package kr.ac.kookmin.stream.db.event; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import kr.ac.kookmin.stream.db.common.BaseTimeEntity; +import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveRelatedLink; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "archive_related_links", + indexes = { + @Index(name = "idx_archive_related_links_archive_id", columnList = "archive_id") + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ArchiveRelatedLinkJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "archive_related_link_id") + private Long id; + + @Column(name = "archive_id", nullable = false) + private Long archiveId; + + @Column(nullable = false, length = 200) + private String title; + + @Column(nullable = false, length = 2048) + private String url; + + @Column(name = "display_order", nullable = false) + private int displayOrder; + + private ArchiveRelatedLinkJpaEntity(ArchiveRelatedLink link) { + this.id = link.getId(); + this.archiveId = link.getArchiveId(); + this.title = link.getTitle(); + this.url = link.getUrl(); + this.displayOrder = link.getDisplayOrder(); + } + + public static ArchiveRelatedLinkJpaEntity from(ArchiveRelatedLink link) { + return new ArchiveRelatedLinkJpaEntity(link); + } + + public ArchiveRelatedLink toDomain() { + return ArchiveRelatedLink.of(id, archiveId, title, url, displayOrder); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventApplicationAnswerJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventApplicationAnswerJpaEntity.java new file mode 100644 index 0000000..063843c --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventApplicationAnswerJpaEntity.java @@ -0,0 +1,61 @@ +package kr.ac.kookmin.stream.db.event; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import java.util.List; +import kr.ac.kookmin.stream.db.common.BaseTimeEntity; +import kr.ac.kookmin.stream.event.domain.event.domain.EventApplicationAnswer; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +@Entity +@Table( + name = "event_application_answers", + indexes = { + @Index(name = "idx_event_application_answers_event_application_id", columnList = "event_application_id") + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EventApplicationAnswerJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "event_application_answer_id") + private Long id; + + @Column(name = "event_application_id", nullable = false) + private Long eventApplicationId; + + @Column(name = "event_question_id", nullable = false) + private Long eventQuestionId; + + @Column(name = "answer_text", columnDefinition = "TEXT") + private String answerText; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "selected_options", columnDefinition = "json") + private List selectedOptions; + + private EventApplicationAnswerJpaEntity(EventApplicationAnswer answer) { + this.id = answer.getId(); + this.eventApplicationId = answer.getEventApplicationId(); + this.eventQuestionId = answer.getEventQuestionId(); + this.answerText = answer.getAnswerText(); + this.selectedOptions = answer.getSelectedOptions(); + } + + public static EventApplicationAnswerJpaEntity from(EventApplicationAnswer answer) { + return new EventApplicationAnswerJpaEntity(answer); + } + + public EventApplicationAnswer toDomain() { + return EventApplicationAnswer.of(id, eventApplicationId, eventQuestionId, answerText, selectedOptions); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventApplicationJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventApplicationJpaEntity.java new file mode 100644 index 0000000..d1bde21 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventApplicationJpaEntity.java @@ -0,0 +1,67 @@ +package kr.ac.kookmin.stream.db.event; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import kr.ac.kookmin.stream.db.common.BaseTimeEntity; +import kr.ac.kookmin.stream.event.domain.event.domain.EventApplication; +import kr.ac.kookmin.stream.event.domain.event.domain.EventApplicationStatus; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "event_applications", + indexes = { + @Index(name = "idx_event_applications_event_id_member_id", columnList = "event_id, member_id"), + @Index(name = "idx_event_applications_member_id", columnList = "member_id") + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EventApplicationJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "event_application_id") + private Long id; + + @Column(name = "event_id", nullable = false) + private Long eventId; + + @Column(name = "member_id", nullable = false) + private Long memberId; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private EventApplicationStatus status; + + @Column(name = "applied_at", nullable = false) + private LocalDateTime appliedAt; + + @Column(name = "canceled_at") + private LocalDateTime canceledAt; + + private EventApplicationJpaEntity(EventApplication application) { + this.id = application.getId(); + this.eventId = application.getEventId(); + this.memberId = application.getMemberId(); + this.status = application.getStatus(); + this.appliedAt = application.getAppliedAt(); + this.canceledAt = application.getCanceledAt(); + } + + public static EventApplicationJpaEntity from(EventApplication application) { + return new EventApplicationJpaEntity(application); + } + + public EventApplication toDomain() { + return EventApplication.of(id, eventId, memberId, status, appliedAt, canceledAt); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventJpaEntity.java new file mode 100644 index 0000000..542c3ce --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventJpaEntity.java @@ -0,0 +1,98 @@ +package kr.ac.kookmin.stream.db.event; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import java.util.List; +import kr.ac.kookmin.stream.db.common.BaseSoftDeleteEntity; +import kr.ac.kookmin.stream.event.domain.event.domain.Event; +import kr.ac.kookmin.stream.event.domain.event.domain.RecruitStatus; +import kr.ac.kookmin.stream.event.domain.event.domain.RecruitType; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +@Entity +@Table(name = "events") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EventJpaEntity extends BaseSoftDeleteEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "event_id") + private Long id; + + @Column(nullable = false, length = 200) + private String title; + + @Column(nullable = false, columnDefinition = "TEXT") + private String description; + + @Column(nullable = false) + private String target; + + private String place; + + @Column(name = "event_start_at", nullable = false) + private LocalDateTime eventStartAt; + + @Column(name = "event_end_at") + private LocalDateTime eventEndAt; + + @Column(name = "apply_start_at", nullable = false) + private LocalDateTime applyStartAt; + + @Column(name = "apply_end_at", nullable = false) + private LocalDateTime applyEndAt; + + @Enumerated(EnumType.STRING) + @Column(name = "recruit_type", nullable = false, length = 30) + private RecruitType recruitType; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "image_ids", columnDefinition = "json") + private List imageIds; + + @Column(nullable = false) + private int capacity; + + @Enumerated(EnumType.STRING) + @Column(name = "recruit_status", nullable = false, length = 30) + private RecruitStatus recruitStatus; + + @Column(name = "created_by", nullable = false) + private Long createdBy; + + private EventJpaEntity(Event event) { + this.id = event.getId(); + this.title = event.getTitle(); + this.description = event.getDescription(); + this.target = event.getTarget(); + this.place = event.getPlace(); + this.eventStartAt = event.getEventStartAt(); + this.eventEndAt = event.getEventEndAt(); + this.applyStartAt = event.getApplyStartAt(); + this.applyEndAt = event.getApplyEndAt(); + this.recruitType = event.getRecruitType(); + this.imageIds = event.getImageIds(); + this.capacity = event.getCapacity(); + this.recruitStatus = event.getRecruitStatus(); + this.createdBy = event.getCreatedBy(); + } + + public static EventJpaEntity from(Event event) { + return new EventJpaEntity(event); + } + + public Event toDomain() { + return Event.of(id, title, description, target, place, eventStartAt, eventEndAt, + applyStartAt, applyEndAt, recruitType, imageIds, capacity, recruitStatus, createdBy); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventQuestionJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventQuestionJpaEntity.java new file mode 100644 index 0000000..ef545f0 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventQuestionJpaEntity.java @@ -0,0 +1,73 @@ +package kr.ac.kookmin.stream.db.event; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import java.util.List; +import kr.ac.kookmin.stream.db.common.BaseTimeEntity; +import kr.ac.kookmin.stream.event.domain.event.domain.EventQuestion; +import kr.ac.kookmin.stream.event.domain.event.domain.QuestionType; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +@Entity +@Table( + name = "event_questions", + indexes = { + @Index(name = "idx_event_questions_event_id", columnList = "event_id") + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EventQuestionJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "event_question_id") + private Long id; + + @Column(name = "event_id", nullable = false) + private Long eventId; + + @Column(name = "question_text", nullable = false, length = 500) + private String questionText; + + @Enumerated(EnumType.STRING) + @Column(name = "question_type", nullable = false, length = 30) + private QuestionType questionType; + + @Column(name = "is_required", nullable = false) + private boolean required; + + @Column(name = "display_order", nullable = false) + private int displayOrder; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "json") + private List options; + + private EventQuestionJpaEntity(EventQuestion question) { + this.id = question.getId(); + this.eventId = question.getEventId(); + this.questionText = question.getQuestionText(); + this.questionType = question.getQuestionType(); + this.required = question.isRequired(); + this.displayOrder = question.getDisplayOrder(); + this.options = question.getOptions(); + } + + public static EventQuestionJpaEntity from(EventQuestion question) { + return new EventQuestionJpaEntity(question); + } + + public EventQuestion toDomain() { + return EventQuestion.of(id, eventId, questionText, questionType, required, displayOrder, options); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerApplicationJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerApplicationJpaEntity.java new file mode 100644 index 0000000..43da179 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerApplicationJpaEntity.java @@ -0,0 +1,54 @@ +package kr.ac.kookmin.stream.db.event; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import kr.ac.kookmin.stream.db.common.BaseCreatedTimeEntity; +import kr.ac.kookmin.stream.event.domain.locker.domain.LockerApplication; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "locker_applications", + indexes = { + @Index(name = "idx_locker_applications_locker_period_id_locker_id", columnList = "locker_period_id, locker_id"), + @Index(name = "idx_locker_applications_member_id", columnList = "member_id") + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LockerApplicationJpaEntity extends BaseCreatedTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "locker_application_id") + private Long id; + + @Column(name = "locker_period_id", nullable = false) + private Long lockerPeriodId; + + @Column(name = "member_id", nullable = false) + private Long memberId; + + @Column(name = "locker_id", nullable = false) + private Long lockerId; + + private LockerApplicationJpaEntity(LockerApplication application) { + this.id = application.getId(); + this.lockerPeriodId = application.getLockerPeriodId(); + this.memberId = application.getMemberId(); + this.lockerId = application.getLockerId(); + } + + public static LockerApplicationJpaEntity from(LockerApplication application) { + return new LockerApplicationJpaEntity(application); + } + + public LockerApplication toDomain() { + return LockerApplication.of(id, lockerPeriodId, memberId, lockerId); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerJpaEntity.java new file mode 100644 index 0000000..31d09d1 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerJpaEntity.java @@ -0,0 +1,59 @@ +package kr.ac.kookmin.stream.db.event; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import kr.ac.kookmin.stream.db.common.BaseSoftDeleteEntity; +import kr.ac.kookmin.stream.event.domain.locker.domain.Locker; +import kr.ac.kookmin.stream.event.domain.locker.domain.LockerStatus; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "lockers") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LockerJpaEntity extends BaseSoftDeleteEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "locker_id") + private Long id; + + @Column(name = "locker_number", nullable = false, length = 20) + private String lockerNumber; + + @Column(nullable = false, length = 20) + private String section; + + @Column(name = "row_no", nullable = false) + private int rowNo; + + @Column(name = "column_no", nullable = false) + private int columnNo; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private LockerStatus status; + + private LockerJpaEntity(Locker locker) { + this.id = locker.getId(); + this.lockerNumber = locker.getLockerNumber(); + this.section = locker.getSection(); + this.rowNo = locker.getRowNo(); + this.columnNo = locker.getColumnNo(); + this.status = locker.getStatus(); + } + + public static LockerJpaEntity from(Locker locker) { + return new LockerJpaEntity(locker); + } + + public Locker toDomain() { + return Locker.of(id, lockerNumber, section, rowNo, columnNo, status); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerPeriodJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerPeriodJpaEntity.java new file mode 100644 index 0000000..98262c4 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerPeriodJpaEntity.java @@ -0,0 +1,57 @@ +package kr.ac.kookmin.stream.db.event; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDate; +import java.time.LocalDateTime; +import kr.ac.kookmin.stream.db.common.BaseTimeEntity; +import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriod; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "locker_periods") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LockerPeriodJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "locker_period_id") + private Long id; + + @Column(nullable = false, length = 100) + private String name; + + @Column(name = "apply_start_at", nullable = false) + private LocalDateTime applyStartAt; + + @Column(name = "apply_end_at", nullable = false) + private LocalDateTime applyEndAt; + + @Column(name = "usage_start_at", nullable = false) + private LocalDate usageStartAt; + + @Column(name = "usage_end_at", nullable = false) + private LocalDate usageEndAt; + + private LockerPeriodJpaEntity(LockerPeriod period) { + this.id = period.getId(); + this.name = period.getName(); + this.applyStartAt = period.getApplyStartAt(); + this.applyEndAt = period.getApplyEndAt(); + this.usageStartAt = period.getUsageStartAt(); + this.usageEndAt = period.getUsageEndAt(); + } + + public static LockerPeriodJpaEntity from(LockerPeriod period) { + return new LockerPeriodJpaEntity(period); + } + + public LockerPeriod toDomain() { + return LockerPeriod.of(id, name, applyStartAt, applyEndAt, usageStartAt, usageEndAt); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/internal/AdminConfigValueJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/internal/AdminConfigValueJpaEntity.java new file mode 100644 index 0000000..4451e29 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/internal/AdminConfigValueJpaEntity.java @@ -0,0 +1,48 @@ +package kr.ac.kookmin.stream.db.internal; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import kr.ac.kookmin.stream.db.common.BaseTimeEntity; +import kr.ac.kookmin.stream.internal.domain.config.domain.AdminConfigValue; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "admin_config_values", + uniqueConstraints = { + @UniqueConstraint(name = "uk_admin_config_values_config_key", columnNames = {"config_key"}) + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AdminConfigValueJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "config_key", nullable = false) + private String configKey; + + @Column(name = "config_value", nullable = false) + private String configValue; + + private AdminConfigValueJpaEntity(AdminConfigValue configValue) { + this.id = configValue.getId(); + this.configKey = configValue.getConfigKey(); + this.configValue = configValue.getConfigValue(); + } + + public static AdminConfigValueJpaEntity from(AdminConfigValue configValue) { + return new AdminConfigValueJpaEntity(configValue); + } + + public AdminConfigValue toDomain() { + return AdminConfigValue.of(id, configKey, configValue); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/internal/DisplayCalendarScheduleJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/internal/DisplayCalendarScheduleJpaEntity.java new file mode 100644 index 0000000..78a5ad2 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/internal/DisplayCalendarScheduleJpaEntity.java @@ -0,0 +1,43 @@ +package kr.ac.kookmin.stream.db.internal; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDate; +import kr.ac.kookmin.stream.db.common.BaseTimeEntity; +import kr.ac.kookmin.stream.internal.domain.display.domain.DisplayCalendarSchedule; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "display_calendar_schedules") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class DisplayCalendarScheduleJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private LocalDate date; + + @Column(nullable = false, columnDefinition = "TEXT") + private String schedules; + + private DisplayCalendarScheduleJpaEntity(DisplayCalendarSchedule schedule) { + this.id = schedule.getId(); + this.date = schedule.getDate(); + this.schedules = schedule.getSchedules(); + } + + public static DisplayCalendarScheduleJpaEntity from(DisplayCalendarSchedule schedule) { + return new DisplayCalendarScheduleJpaEntity(schedule); + } + + public DisplayCalendarSchedule toDomain() { + return DisplayCalendarSchedule.of(id, date, schedules); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/internal/DisplayPosterJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/internal/DisplayPosterJpaEntity.java new file mode 100644 index 0000000..5c0e3dd --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/internal/DisplayPosterJpaEntity.java @@ -0,0 +1,46 @@ +package kr.ac.kookmin.stream.db.internal; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import kr.ac.kookmin.stream.db.common.BaseCreatedTimeEntity; +import kr.ac.kookmin.stream.internal.domain.display.domain.DisplayPoster; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "display_posters") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class DisplayPosterJpaEntity extends BaseCreatedTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column(name = "image_key", nullable = false) + private String imageKey; + + @Column(name = "is_active", nullable = false) + private boolean active; + + private DisplayPosterJpaEntity(DisplayPoster poster) { + this.id = poster.getId(); + this.title = poster.getTitle(); + this.imageKey = poster.getImageKey(); + this.active = poster.isActive(); + } + + public static DisplayPosterJpaEntity from(DisplayPoster poster) { + return new DisplayPosterJpaEntity(poster); + } + + public DisplayPoster toDomain() { + return DisplayPoster.of(id, title, imageKey, active); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/internal/FileJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/internal/FileJpaEntity.java new file mode 100644 index 0000000..ff9166c --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/internal/FileJpaEntity.java @@ -0,0 +1,63 @@ +package kr.ac.kookmin.stream.db.internal; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import kr.ac.kookmin.stream.db.common.BaseCreatedTimeEntity; +import kr.ac.kookmin.stream.internal.domain.file.domain.File; +import kr.ac.kookmin.stream.internal.domain.file.domain.FileCategory; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "files") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class FileJpaEntity extends BaseCreatedTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "file_id") + private Long id; + + @Column(name = "file_key", nullable = false, columnDefinition = "TEXT") + private String fileKey; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 30) + private FileCategory category; + + @Column(name = "original_name", nullable = false) + private String originalName; + + @Column(name = "file_size", nullable = false) + private long fileSize; + + @Column(name = "content_type", nullable = false, length = 100) + private String contentType; + + @Column(name = "uploader_id", nullable = false) + private Long uploaderId; + + private FileJpaEntity(File file) { + this.id = file.getId(); + this.fileKey = file.getFileKey(); + this.category = file.getCategory(); + this.originalName = file.getOriginalName(); + this.fileSize = file.getFileSize(); + this.contentType = file.getContentType(); + this.uploaderId = file.getUploaderId(); + } + + public static FileJpaEntity from(File file) { + return new FileJpaEntity(file); + } + + public File toDomain() { + return File.of(id, fileKey, category, originalName, fileSize, contentType, uploaderId); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/internal/WorkScheduleJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/internal/WorkScheduleJpaEntity.java new file mode 100644 index 0000000..024a5ab --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/internal/WorkScheduleJpaEntity.java @@ -0,0 +1,57 @@ +package kr.ac.kookmin.stream.db.internal; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import kr.ac.kookmin.stream.db.common.BaseTimeEntity; +import kr.ac.kookmin.stream.internal.domain.schedule.domain.WorkSchedule; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "work_schedules", + indexes = { + @Index(name = "idx_work_schedules_member_id", columnList = "member_id") + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class WorkScheduleJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "schedule_id") + private Long id; + + @Column(name = "member_id", nullable = false) + private Long memberId; + + @Column(name = "day_of_week", nullable = false, length = 10) + private String dayOfWeek; + + @Column(name = "start_hour", nullable = false, columnDefinition = "TINYINT") + private int startHour; + + @Column(name = "end_hour", nullable = false, columnDefinition = "TINYINT") + private int endHour; + + private WorkScheduleJpaEntity(WorkSchedule schedule) { + this.id = schedule.getId(); + this.memberId = schedule.getMemberId(); + this.dayOfWeek = schedule.getDayOfWeek(); + this.startHour = schedule.getStartHour(); + this.endHour = schedule.getEndHour(); + } + + public static WorkScheduleJpaEntity from(WorkSchedule schedule) { + return new WorkScheduleJpaEntity(schedule); + } + + public WorkSchedule toDomain() { + return WorkSchedule.of(id, memberId, dayOfWeek, startHour, endHour); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaEntity.java index b7d09d0..3c3d363 100644 --- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaEntity.java +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaEntity.java @@ -1,31 +1,69 @@ package kr.ac.kookmin.stream.db.member; +import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; +import kr.ac.kookmin.stream.common.CouncilDepartment; +import kr.ac.kookmin.stream.common.Role; import kr.ac.kookmin.stream.db.common.BaseSoftDeleteEntity; -import kr.ac.kookmin.stream.member.Member; +import kr.ac.kookmin.stream.member.domain.member.domain.Department; +import kr.ac.kookmin.stream.member.domain.member.domain.Member; import lombok.AccessLevel; import lombok.NoArgsConstructor; @Entity +// uk_members_active_student_id(V1)는 generated column active_student_id에 걸려 있다. +// Entity에 매핑된 컬럼이 아니라 여기서는 선언하지 않는다. @Table(name = "members") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class MemberJpaEntity extends BaseSoftDeleteEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "member_id") private Long id; - private String studentNo; + @Column(name = "student_id", nullable = false) + private String studentId; + + @Column(nullable = false) private String name; + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 30) + private Department department; + + private String email; + + @Column(name = "fcm_token") + private String fcmToken; + + @Column(name = "is_fee_paid", nullable = false) + private boolean feePaid; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private Role role; + + @Enumerated(EnumType.STRING) + @Column(name = "council_department", length = 30) + private CouncilDepartment councilDepartment; + private MemberJpaEntity(Member member) { - this.id = member.id(); - this.studentNo = member.studentNo(); - this.name = member.name(); + this.id = member.getId(); + this.studentId = member.getStudentId(); + this.name = member.getName(); + this.department = member.getDepartment(); + this.email = member.getEmail(); + this.fcmToken = member.getFcmToken(); + this.feePaid = member.isFeePaid(); + this.role = member.getRole(); + this.councilDepartment = member.getCouncilDepartment(); } public static MemberJpaEntity from(Member member) { @@ -33,6 +71,6 @@ public static MemberJpaEntity from(Member member) { } public Member toDomain() { - return new Member(id, studentNo, name); + return Member.of(id, studentId, name, department, email, fcmToken, feePaid, role, councilDepartment); } } diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaRepository.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaRepository.java index 13d7092..64e2814 100644 --- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaRepository.java +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberJpaRepository.java @@ -4,5 +4,5 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface MemberJpaRepository extends JpaRepository { - Optional findByIdAndDeletedAtIsNull(Long id); + Optional findByIdAndIsDeletedFalse(Long id); } diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberNotificationSettingJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberNotificationSettingJpaEntity.java new file mode 100644 index 0000000..2a10a82 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberNotificationSettingJpaEntity.java @@ -0,0 +1,61 @@ +package kr.ac.kookmin.stream.db.member; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import kr.ac.kookmin.stream.db.common.BaseTimeEntity; +import kr.ac.kookmin.stream.member.domain.notification.domain.MemberNotificationSetting; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "member_notification_settings", + uniqueConstraints = { + @UniqueConstraint(name = "uk_member_notification_settings_member_id", columnNames = {"member_id"}) + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MemberNotificationSettingJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "member_notification_setting_id") + private Long id; + + @Column(name = "member_id", nullable = false) + private Long memberId; + + @Column(name = "rental_enabled", nullable = false) + private boolean rentalEnabled; + + @Column(name = "event_enabled", nullable = false) + private boolean eventEnabled; + + @Column(name = "locker_enabled", nullable = false) + private boolean lockerEnabled; + + @Column(name = "notice_enabled", nullable = false) + private boolean noticeEnabled; + + private MemberNotificationSettingJpaEntity(MemberNotificationSetting setting) { + this.id = setting.getId(); + this.memberId = setting.getMemberId(); + this.rentalEnabled = setting.isRentalEnabled(); + this.eventEnabled = setting.isEventEnabled(); + this.lockerEnabled = setting.isLockerEnabled(); + this.noticeEnabled = setting.isNoticeEnabled(); + } + + public static MemberNotificationSettingJpaEntity from(MemberNotificationSetting setting) { + return new MemberNotificationSettingJpaEntity(setting); + } + + public MemberNotificationSetting toDomain() { + return MemberNotificationSetting.of(id, memberId, rentalEnabled, eventEnabled, lockerEnabled, noticeEnabled); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberRepositoryImpl.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberRepositoryImpl.java index 06b8b81..3d4a1ab 100644 --- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberRepositoryImpl.java +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberRepositoryImpl.java @@ -1,8 +1,8 @@ package kr.ac.kookmin.stream.db.member; import java.util.Optional; -import kr.ac.kookmin.stream.member.Member; -import kr.ac.kookmin.stream.member.MemberRepository; +import kr.ac.kookmin.stream.member.domain.member.domain.Member; +import kr.ac.kookmin.stream.member.domain.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -14,6 +14,6 @@ public class MemberRepositoryImpl implements MemberRepository { @Override public Optional findById(Long id) { - return memberJpaRepository.findByIdAndDeletedAtIsNull(id).map(MemberJpaEntity::toDomain); + return memberJpaRepository.findByIdAndIsDeletedFalse(id).map(MemberJpaEntity::toDomain); } } diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberTermAgreementJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberTermAgreementJpaEntity.java new file mode 100644 index 0000000..62bd7c0 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/MemberTermAgreementJpaEntity.java @@ -0,0 +1,69 @@ +package kr.ac.kookmin.stream.db.member; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import java.time.LocalDateTime; +import kr.ac.kookmin.stream.db.common.BaseTimeEntity; +import kr.ac.kookmin.stream.member.domain.member.domain.MemberTermAgreement; +import kr.ac.kookmin.stream.member.domain.member.domain.TermType; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "member_term_agreements", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_member_term_agreements_member_id_term_type", + columnNames = {"member_id", "term_type"} + ) + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MemberTermAgreementJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "member_term_agreement_id") + private Long id; + + @Column(name = "member_id", nullable = false) + private Long memberId; + + @Enumerated(EnumType.STRING) + @Column(name = "term_type", nullable = false, length = 30) + private TermType termType; + + @Column(name = "term_version", nullable = false, length = 20) + private String termVersion; + + @Column(nullable = false) + private boolean agreed; + + @Column(name = "agreed_at") + private LocalDateTime agreedAt; + + private MemberTermAgreementJpaEntity(MemberTermAgreement agreement) { + this.id = agreement.getId(); + this.memberId = agreement.getMemberId(); + this.termType = agreement.getTermType(); + this.termVersion = agreement.getTermVersion(); + this.agreed = agreement.isAgreed(); + this.agreedAt = agreement.getAgreedAt(); + } + + public static MemberTermAgreementJpaEntity from(MemberTermAgreement agreement) { + return new MemberTermAgreementJpaEntity(agreement); + } + + public MemberTermAgreement toDomain() { + return MemberTermAgreement.of(id, memberId, termType, termVersion, agreed, agreedAt); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/NotificationJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/NotificationJpaEntity.java new file mode 100644 index 0000000..66571a9 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/member/NotificationJpaEntity.java @@ -0,0 +1,57 @@ +package kr.ac.kookmin.stream.db.member; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import kr.ac.kookmin.stream.db.common.BaseCreatedTimeEntity; +import kr.ac.kookmin.stream.member.domain.notification.domain.Notification; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "notifications", + indexes = { + @Index(name = "idx_notifications_member_id", columnList = "member_id") + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class NotificationJpaEntity extends BaseCreatedTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "notification_id") + private Long id; + + @Column(name = "member_id", nullable = false) + private Long memberId; + + @Column(nullable = false, length = 50) + private String type; + + @Column(name = "format_values") + private String formatValues; + + @Column(name = "is_read", nullable = false) + private boolean read; + + private NotificationJpaEntity(Notification notification) { + this.id = notification.getId(); + this.memberId = notification.getMemberId(); + this.type = notification.getType(); + this.formatValues = notification.getFormatValues(); + this.read = notification.isRead(); + } + + public static NotificationJpaEntity from(Notification notification) { + return new NotificationJpaEntity(notification); + } + + public Notification toDomain() { + return Notification.of(id, memberId, type, formatValues, read); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/ChatMessageJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/ChatMessageJpaEntity.java new file mode 100644 index 0000000..fa7f1be --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/ChatMessageJpaEntity.java @@ -0,0 +1,53 @@ +package kr.ac.kookmin.stream.db.welfare; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import kr.ac.kookmin.stream.db.common.BaseCreatedTimeEntity; +import kr.ac.kookmin.stream.welfare.domain.chat.domain.ChatMessage; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "chat_messages", + indexes = { + @Index(name = "idx_chat_messages_member_id", columnList = "member_id") + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ChatMessageJpaEntity extends BaseCreatedTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "message_id") + private Long id; + + @Column(name = "member_id", nullable = false) + private Long memberId; + + @Column(name = "sender_type", nullable = false, length = 10) + private String senderType; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; + + private ChatMessageJpaEntity(ChatMessage message) { + this.id = message.getId(); + this.memberId = message.getMemberId(); + this.senderType = message.getSenderType(); + this.content = message.getContent(); + } + + public static ChatMessageJpaEntity from(ChatMessage message) { + return new ChatMessageJpaEntity(message); + } + + public ChatMessage toDomain() { + return ChatMessage.of(id, memberId, senderType, content); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/ItemJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/ItemJpaEntity.java new file mode 100644 index 0000000..ce460bb --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/ItemJpaEntity.java @@ -0,0 +1,54 @@ +package kr.ac.kookmin.stream.db.welfare; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import kr.ac.kookmin.stream.db.common.BaseTimeEntity; +import kr.ac.kookmin.stream.welfare.domain.rental.domain.Item; +import kr.ac.kookmin.stream.welfare.domain.rental.domain.ItemType; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "items") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ItemJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 30) + private ItemType type; + + @Column(nullable = false) + private int count; + + @Column(name = "image_key") + private String imageKey; + + private ItemJpaEntity(Item item) { + this.id = item.getId(); + this.name = item.getName(); + this.type = item.getType(); + this.count = item.getCount(); + this.imageKey = item.getImageKey(); + } + + public static ItemJpaEntity from(Item item) { + return new ItemJpaEntity(item); + } + + public Item toDomain() { + return Item.of(id, name, type, count, imageKey); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/NoticeJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/NoticeJpaEntity.java new file mode 100644 index 0000000..3e80da0 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/NoticeJpaEntity.java @@ -0,0 +1,78 @@ +package kr.ac.kookmin.stream.db.welfare; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import java.util.List; +import kr.ac.kookmin.stream.db.common.BaseSoftDeleteEntity; +import kr.ac.kookmin.stream.welfare.domain.notice.domain.Notice; +import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeCategory; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +@Entity +@Table( + name = "notices", + indexes = { + @Index(name = "idx_notices_category_is_deleted", columnList = "category, is_deleted") + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class NoticeJpaEntity extends BaseSoftDeleteEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "notice_id") + private Long id; + + @Column(nullable = false, length = 200) + private String title; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 30) + private NoticeCategory category; + + @Column(nullable = false) + private boolean pinned; + + @Column(name = "created_by") + private Long createdBy; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "attachment_ids", columnDefinition = "json") + private List attachmentIds; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "image_ids", columnDefinition = "json") + private List imageIds; + + private NoticeJpaEntity(Notice notice) { + this.id = notice.getId(); + this.title = notice.getTitle(); + this.content = notice.getContent(); + this.category = notice.getCategory(); + this.pinned = notice.isPinned(); + this.createdBy = notice.getCreatedBy(); + this.attachmentIds = notice.getAttachmentIds(); + this.imageIds = notice.getImageIds(); + } + + public static NoticeJpaEntity from(Notice notice) { + return new NoticeJpaEntity(notice); + } + + public Notice toDomain() { + return Notice.of(id, title, content, category, pinned, createdBy, attachmentIds, imageIds); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/OpenFeedbackJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/OpenFeedbackJpaEntity.java new file mode 100644 index 0000000..57ab15e --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/OpenFeedbackJpaEntity.java @@ -0,0 +1,64 @@ +package kr.ac.kookmin.stream.db.welfare; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import kr.ac.kookmin.stream.db.common.BaseSoftDeleteEntity; +import kr.ac.kookmin.stream.welfare.domain.feedback.domain.OpenFeedback; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "open_feedbacks") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class OpenFeedbackJpaEntity extends BaseSoftDeleteEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "feedback_id") + private Long id; + + @Column(nullable = false) + private int round; + + @Column(nullable = false, length = 20) + private String category; + + @Column(nullable = false, length = 1000) + private String question; + + @Column(columnDefinition = "TEXT") + private String answer; + + @Column(name = "answered_at") + private LocalDateTime answeredAt; + + @Column(name = "created_by", nullable = false) + private Long createdBy; + + @Column(name = "answered_by") + private Long answeredBy; + + private OpenFeedbackJpaEntity(OpenFeedback feedback) { + this.id = feedback.getId(); + this.round = feedback.getRound(); + this.category = feedback.getCategory(); + this.question = feedback.getQuestion(); + this.answer = feedback.getAnswer(); + this.answeredAt = feedback.getAnsweredAt(); + this.createdBy = feedback.getCreatedBy(); + this.answeredBy = feedback.getAnsweredBy(); + } + + public static OpenFeedbackJpaEntity from(OpenFeedback feedback) { + return new OpenFeedbackJpaEntity(feedback); + } + + public OpenFeedback toDomain() { + return OpenFeedback.of(id, round, category, question, answer, answeredAt, createdBy, answeredBy); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/PayerJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/PayerJpaEntity.java new file mode 100644 index 0000000..5c86682 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/PayerJpaEntity.java @@ -0,0 +1,61 @@ +package kr.ac.kookmin.stream.db.welfare; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import kr.ac.kookmin.stream.db.common.BaseTimeEntity; +import kr.ac.kookmin.stream.welfare.domain.fee.domain.Payer; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "payers", + indexes = { + @Index(name = "idx_payers_member_id", columnList = "member_id") + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PayerJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "payer_id") + private Long id; + + @Column(name = "member_id", nullable = false) + private Long memberId; + + @Column(nullable = false) + private String name; + + @Column(name = "student_id") + private String studentId; + + @Column(name = "enrollment_year", nullable = false) + private String enrollmentYear; + + @Column(nullable = false) + private boolean registered; + + private PayerJpaEntity(Payer payer) { + this.id = payer.getId(); + this.memberId = payer.getMemberId(); + this.name = payer.getName(); + this.studentId = payer.getStudentId(); + this.enrollmentYear = payer.getEnrollmentYear(); + this.registered = payer.isRegistered(); + } + + public static PayerJpaEntity from(Payer payer) { + return new PayerJpaEntity(payer); + } + + public Payer toDomain() { + return Payer.of(id, memberId, name, studentId, enrollmentYear, registered); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/RentalHistoryJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/RentalHistoryJpaEntity.java new file mode 100644 index 0000000..2cb8bd2 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/RentalHistoryJpaEntity.java @@ -0,0 +1,84 @@ +package kr.ac.kookmin.stream.db.welfare; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import kr.ac.kookmin.stream.db.common.BaseTimeEntity; +import kr.ac.kookmin.stream.welfare.domain.rental.domain.RentalHistory; +import kr.ac.kookmin.stream.welfare.domain.rental.domain.RentalStatus; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "rental_histories", + indexes = { + @Index(name = "idx_rental_histories_member_id", columnList = "member_id"), + @Index(name = "idx_rental_histories_item_id", columnList = "item_id") + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RentalHistoryJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "history_id") + private Long id; + + @Column(name = "item_id", nullable = false) + private Long itemId; + + @Column(name = "member_id", nullable = false) + private Long memberId; + + @Column(name = "worker_id") + private Long workerId; + + @Column(name = "item_code") + private String itemCode; + + @Enumerated(EnumType.STRING) + @Column(name = "rental_status", nullable = false, length = 30) + private RentalStatus rentalStatus; + + @Column(name = "rented_count", nullable = false) + private int rentedCount; + + @Column(name = "applied_at", nullable = false) + private LocalDateTime appliedAt; + + @Column(name = "rent_at") + private LocalDateTime rentAt; + + @Column(name = "returned_at") + private LocalDateTime returnedAt; + + private RentalHistoryJpaEntity(RentalHistory history) { + this.id = history.getId(); + this.itemId = history.getItemId(); + this.memberId = history.getMemberId(); + this.workerId = history.getWorkerId(); + this.itemCode = history.getItemCode(); + this.rentalStatus = history.getRentalStatus(); + this.rentedCount = history.getRentedCount(); + this.appliedAt = history.getAppliedAt(); + this.rentAt = history.getRentAt(); + this.returnedAt = history.getReturnedAt(); + } + + public static RentalHistoryJpaEntity from(RentalHistory history) { + return new RentalHistoryJpaEntity(history); + } + + public RentalHistory toDomain() { + return RentalHistory.of(id, itemId, memberId, workerId, itemCode, rentalStatus, + rentedCount, appliedAt, rentAt, returnedAt); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/RentalStatusWorkerLogJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/RentalStatusWorkerLogJpaEntity.java new file mode 100644 index 0000000..7441a5f --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/RentalStatusWorkerLogJpaEntity.java @@ -0,0 +1,57 @@ +package kr.ac.kookmin.stream.db.welfare; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import kr.ac.kookmin.stream.db.common.BaseCreatedTimeEntity; +import kr.ac.kookmin.stream.welfare.domain.rental.domain.RentalStatus; +import kr.ac.kookmin.stream.welfare.domain.rental.domain.RentalStatusWorkerLog; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "rental_status_worker_logs", + indexes = { + @Index(name = "idx_rental_status_worker_logs_rental_history_id", columnList = "rental_history_id") + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RentalStatusWorkerLogJpaEntity extends BaseCreatedTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "rental_status_worker_log_id") + private Long id; + + @Column(name = "rental_history_id", nullable = false) + private Long rentalHistoryId; + + @Column(name = "worker_id", nullable = false) + private Long workerId; + + @Enumerated(EnumType.STRING) + @Column(name = "rental_status", nullable = false, length = 30) + private RentalStatus rentalStatus; + + private RentalStatusWorkerLogJpaEntity(RentalStatusWorkerLog log) { + this.id = log.getId(); + this.rentalHistoryId = log.getRentalHistoryId(); + this.workerId = log.getWorkerId(); + this.rentalStatus = log.getRentalStatus(); + } + + public static RentalStatusWorkerLogJpaEntity from(RentalStatusWorkerLog log) { + return new RentalStatusWorkerLogJpaEntity(log); + } + + public RentalStatusWorkerLog toDomain() { + return RentalStatusWorkerLog.of(id, rentalHistoryId, workerId, rentalStatus); + } +} diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/StudentFeeJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/StudentFeeJpaEntity.java new file mode 100644 index 0000000..91737f5 --- /dev/null +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/StudentFeeJpaEntity.java @@ -0,0 +1,70 @@ +package kr.ac.kookmin.stream.db.welfare; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import kr.ac.kookmin.stream.db.common.BaseTimeEntity; +import kr.ac.kookmin.stream.welfare.domain.fee.domain.PaymentStatus; +import kr.ac.kookmin.stream.welfare.domain.fee.domain.StudentFee; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "student_fees", + indexes = { + @Index(name = "idx_student_fees_member_id", columnList = "member_id") + } +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class StudentFeeJpaEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "student_fee_id") + private Long id; + + @Column(name = "member_id", nullable = false) + private Long memberId; + + @Column(nullable = false) + private int amount; + + @Enumerated(EnumType.STRING) + @Column(name = "payment_status", nullable = false, length = 20) + private PaymentStatus paymentStatus; + + @Column(name = "payment_link_url", length = 500) + private String paymentLinkUrl; + + @Column(name = "paid_at") + private LocalDateTime paidAt; + + @Column(name = "confirmed_by") + private Long confirmedBy; + + private StudentFeeJpaEntity(StudentFee fee) { + this.id = fee.getId(); + this.memberId = fee.getMemberId(); + this.amount = fee.getAmount(); + this.paymentStatus = fee.getPaymentStatus(); + this.paymentLinkUrl = fee.getPaymentLinkUrl(); + this.paidAt = fee.getPaidAt(); + this.confirmedBy = fee.getConfirmedBy(); + } + + public static StudentFeeJpaEntity from(StudentFee fee) { + return new StudentFeeJpaEntity(fee); + } + + public StudentFee toDomain() { + return StudentFee.of(id, memberId, amount, paymentStatus, paymentLinkUrl, paidAt, confirmedBy); + } +} diff --git a/infrastructure/db/src/main/resources/application-infrastructure-db.yml b/infrastructure/db/src/main/resources/application-infrastructure-db.yml new file mode 100644 index 0000000..b841725 --- /dev/null +++ b/infrastructure/db/src/main/resources/application-infrastructure-db.yml @@ -0,0 +1,11 @@ +spring: + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + jpa: + hibernate: + ddl-auto: validate + flyway: + enabled: true + locations: classpath:db/migration diff --git a/infrastructure/db/src/main/resources/db/migration/V1__create_member_tables.sql b/infrastructure/db/src/main/resources/db/migration/V1__create_member_tables.sql new file mode 100644 index 0000000..69774e5 --- /dev/null +++ b/infrastructure/db/src/main/resources/db/migration/V1__create_member_tables.sql @@ -0,0 +1,56 @@ +CREATE TABLE members ( + member_id BIGINT AUTO_INCREMENT PRIMARY KEY, + student_id VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, + department VARCHAR(30) NOT NULL, -- AI / SW + email VARCHAR(255) NULL, + fcm_token VARCHAR(255) NULL, + is_fee_paid TINYINT(1) NOT NULL DEFAULT 0, + role VARCHAR(20) NOT NULL DEFAULT 'STUDENT', -- STUDENT / ADMIN + council_department VARCHAR(30) NULL, -- CouncilDepartment.name() + is_deleted TINYINT(1) NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 활성 학번만 유니크 (삭제 후 재등록 허용) +ALTER TABLE members + ADD COLUMN active_student_id VARCHAR(255) + GENERATED ALWAYS AS (IF(is_deleted = 0, student_id, NULL)) VIRTUAL; +CREATE UNIQUE INDEX uk_members_active_student_id ON members (active_student_id); + +CREATE TABLE member_term_agreements ( + member_term_agreement_id BIGINT AUTO_INCREMENT PRIMARY KEY, + member_id BIGINT NOT NULL, + term_type VARCHAR(30) NOT NULL, -- PRIVACY_POLICY / TERMS_OF_SERVICE + term_version VARCHAR(20) NOT NULL, + agreed TINYINT(1) NOT NULL DEFAULT 0, + agreed_at DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX uk_member_term_agreements_member_id_term_type + ON member_term_agreements (member_id, term_type); + +CREATE TABLE member_notification_settings ( + member_notification_setting_id BIGINT AUTO_INCREMENT PRIMARY KEY, + member_id BIGINT NOT NULL, + rental_enabled TINYINT(1) NOT NULL DEFAULT 1, + event_enabled TINYINT(1) NOT NULL DEFAULT 1, + locker_enabled TINYINT(1) NOT NULL DEFAULT 1, + notice_enabled TINYINT(1) NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX uk_member_notification_settings_member_id + ON member_notification_settings (member_id); + +CREATE TABLE notifications ( + notification_id BIGINT AUTO_INCREMENT PRIMARY KEY, + member_id BIGINT NOT NULL, + type VARCHAR(50) NOT NULL, + format_values VARCHAR(255) NULL, + is_read TINYINT(1) NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_notifications_member_id ON notifications (member_id); diff --git a/infrastructure/db/src/main/resources/db/migration/V2__create_event_tables.sql b/infrastructure/db/src/main/resources/db/migration/V2__create_event_tables.sql new file mode 100644 index 0000000..10e8625 --- /dev/null +++ b/infrastructure/db/src/main/resources/db/migration/V2__create_event_tables.sql @@ -0,0 +1,115 @@ +CREATE TABLE events ( + event_id BIGINT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(200) NOT NULL, + description TEXT NOT NULL, + target VARCHAR(255) NOT NULL, + place VARCHAR(255) NULL, + event_start_at DATETIME NOT NULL, + event_end_at DATETIME NULL, + apply_start_at DATETIME NOT NULL, + apply_end_at DATETIME NOT NULL, + recruit_type VARCHAR(30) NOT NULL, -- FIRST_COME / OPEN + image_ids JSON NULL, + capacity INT NOT NULL, + recruit_status VARCHAR(30) NOT NULL DEFAULT 'BEFORE_OPEN', -- BEFORE_OPEN / OPEN / CLOSED + created_by BIGINT NOT NULL, + is_deleted TINYINT(1) NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE event_questions ( + event_question_id BIGINT AUTO_INCREMENT PRIMARY KEY, + event_id BIGINT NOT NULL, + question_text VARCHAR(500) NOT NULL, + question_type VARCHAR(30) NOT NULL, -- SHORT_TEXT / LONG_TEXT / SINGLE_CHOICE / MULTIPLE_CHOICE + is_required TINYINT(1) NOT NULL DEFAULT 0, + display_order INT NOT NULL, + options JSON NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +CREATE INDEX idx_event_questions_event_id ON event_questions (event_id); + +CREATE TABLE event_applications ( + event_application_id BIGINT AUTO_INCREMENT PRIMARY KEY, + event_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'APPLIED', -- APPLIED / CANCELED + applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + canceled_at DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +CREATE INDEX idx_event_applications_event_id_member_id ON event_applications (event_id, member_id); +CREATE INDEX idx_event_applications_member_id ON event_applications (member_id); + +CREATE TABLE event_application_answers ( + event_application_answer_id BIGINT AUTO_INCREMENT PRIMARY KEY, + event_application_id BIGINT NOT NULL, + event_question_id BIGINT NOT NULL, + answer_text TEXT NULL, + selected_options JSON NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +CREATE INDEX idx_event_application_answers_event_application_id + ON event_application_answers (event_application_id); + +CREATE TABLE lockers ( + locker_id BIGINT AUTO_INCREMENT PRIMARY KEY, + locker_number VARCHAR(20) NOT NULL, + section VARCHAR(20) NOT NULL, + row_no INT NOT NULL, + column_no INT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'AVAILABLE', -- AVAILABLE / DISABLED + is_deleted TINYINT(1) NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE locker_periods ( + locker_period_id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + apply_start_at DATETIME NOT NULL, + apply_end_at DATETIME NOT NULL, + usage_start_at DATE NOT NULL, + usage_end_at DATE NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE locker_applications ( + locker_application_id BIGINT AUTO_INCREMENT PRIMARY KEY, + locker_period_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, + locker_id BIGINT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_locker_applications_locker_period_id_locker_id + ON locker_applications (locker_period_id, locker_id); +CREATE INDEX idx_locker_applications_member_id ON locker_applications (member_id); + +CREATE TABLE archives ( + archive_id BIGINT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(200) NOT NULL, + content TEXT NOT NULL, + start_date DATE NOT NULL, + end_date DATE NULL, + location VARCHAR(255) NULL, + department_name VARCHAR(100) NOT NULL, + image_ids JSON NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE archive_related_links ( + archive_related_link_id BIGINT AUTO_INCREMENT PRIMARY KEY, + archive_id BIGINT NOT NULL, + title VARCHAR(200) NOT NULL, + url VARCHAR(2048) NOT NULL, + display_order INT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +CREATE INDEX idx_archive_related_links_archive_id ON archive_related_links (archive_id); diff --git a/infrastructure/db/src/main/resources/db/migration/V3__create_welfare_tables.sql b/infrastructure/db/src/main/resources/db/migration/V3__create_welfare_tables.sql new file mode 100644 index 0000000..8165c12 --- /dev/null +++ b/infrastructure/db/src/main/resources/db/migration/V3__create_welfare_tables.sql @@ -0,0 +1,99 @@ +CREATE TABLE items ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + type VARCHAR(30) NOT NULL, -- CONSUMABLE / RENTAL + count INT NOT NULL, + image_key VARCHAR(255) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE rental_histories ( + history_id BIGINT AUTO_INCREMENT PRIMARY KEY, + item_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, + worker_id BIGINT NULL, + item_code VARCHAR(255) NULL, + rental_status VARCHAR(30) NOT NULL, -- CANCEL / CONFIRMED / PENDING / REJECTED / RENTAL / RETURNED / RETURN_CONFIRMED / RETURN_PENDING + rented_count INT NOT NULL, + applied_at DATETIME NOT NULL, + rent_at DATETIME NULL, + returned_at DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +CREATE INDEX idx_rental_histories_member_id ON rental_histories (member_id); +CREATE INDEX idx_rental_histories_item_id ON rental_histories (item_id); + +CREATE TABLE rental_status_worker_logs ( + rental_status_worker_log_id BIGINT AUTO_INCREMENT PRIMARY KEY, + rental_history_id BIGINT NOT NULL, + worker_id BIGINT NOT NULL, + rental_status VARCHAR(30) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_rental_status_worker_logs_rental_history_id + ON rental_status_worker_logs (rental_history_id); + +CREATE TABLE student_fees ( + student_fee_id BIGINT AUTO_INCREMENT PRIMARY KEY, + member_id BIGINT NOT NULL, + amount INT NOT NULL, + payment_status VARCHAR(20) NOT NULL DEFAULT 'UNPAID', -- UNPAID / PAID + payment_link_url VARCHAR(500) NULL, + paid_at DATETIME NULL, + confirmed_by BIGINT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +CREATE INDEX idx_student_fees_member_id ON student_fees (member_id); + +CREATE TABLE payers ( + payer_id BIGINT AUTO_INCREMENT PRIMARY KEY, + member_id BIGINT NOT NULL, + name VARCHAR(255) NOT NULL, + student_id VARCHAR(255) NULL, + enrollment_year VARCHAR(255) NOT NULL, + registered TINYINT(1) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +CREATE INDEX idx_payers_member_id ON payers (member_id); + +CREATE TABLE notices ( + notice_id BIGINT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(200) NOT NULL, + content TEXT NOT NULL, + category VARCHAR(30) NOT NULL, -- GENERAL / PARTNERSHIP + pinned TINYINT(1) NOT NULL DEFAULT 0, + created_by BIGINT NULL, + attachment_ids JSON NULL, + image_ids JSON NULL, + is_deleted TINYINT(1) NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +CREATE INDEX idx_notices_category_is_deleted ON notices (category, is_deleted); + +CREATE TABLE chat_messages ( + message_id BIGINT AUTO_INCREMENT PRIMARY KEY, + member_id BIGINT NOT NULL, + sender_type VARCHAR(10) NOT NULL, + content TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX idx_chat_messages_member_id ON chat_messages (member_id); + +CREATE TABLE open_feedbacks ( + feedback_id BIGINT AUTO_INCREMENT PRIMARY KEY, + round INT NOT NULL, + category VARCHAR(20) NOT NULL, + question VARCHAR(1000) NOT NULL, + answer TEXT NULL, + answered_at DATETIME NULL, + created_by BIGINT NOT NULL, + answered_by BIGINT NULL, + is_deleted TINYINT(1) NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); diff --git a/infrastructure/db/src/main/resources/db/migration/V4__create_internal_tables.sql b/infrastructure/db/src/main/resources/db/migration/V4__create_internal_tables.sql new file mode 100644 index 0000000..2472f9e --- /dev/null +++ b/infrastructure/db/src/main/resources/db/migration/V4__create_internal_tables.sql @@ -0,0 +1,46 @@ +CREATE TABLE work_schedules ( + schedule_id BIGINT AUTO_INCREMENT PRIMARY KEY, + member_id BIGINT NOT NULL, + day_of_week VARCHAR(10) NOT NULL, + start_hour TINYINT NOT NULL, + end_hour TINYINT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +CREATE INDEX idx_work_schedules_member_id ON work_schedules (member_id); + +CREATE TABLE admin_config_values ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + config_key VARCHAR(255) NOT NULL, + config_value VARCHAR(255) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX uk_admin_config_values_config_key ON admin_config_values (config_key); + +CREATE TABLE display_posters ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + image_key VARCHAR(255) NOT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE display_calendar_schedules ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + date DATE NOT NULL, + schedules TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE files ( + file_id BIGINT AUTO_INCREMENT PRIMARY KEY, + file_key TEXT NOT NULL, + category VARCHAR(30) NOT NULL, -- TEMP / NOTICE_ATTACHMENT / NOTICE_IMAGE / EVENT_IMAGE / ARCHIVE_IMAGE + original_name VARCHAR(255) NOT NULL, + file_size BIGINT NOT NULL, + content_type VARCHAR(100) NOT NULL, + uploader_id BIGINT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +);