diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 0b2ba913c..7a3738b58 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,23 +1,23 @@
-## PR ๋ด์ฉ
+# ๐ PR ๋ด์ฉ
-## PR ์ธ๋ถ์ฌํญ
+## ๐ PR ์ธ๋ถ์ฌํญ
-## ๊ด๋ จ ์คํฌ๋ฆฐ์ท
+## ๐ธ ๊ด๋ จ ์คํฌ๋ฆฐ์ท
-## ์ฃผ์์ฌํญ
+## ๐ ์ฃผ์์ฌํญ
-## ์ฒดํฌ ๋ฆฌ์คํธ
+## โ
์ฒดํฌ๋ฆฌ์คํธ
- [ ] ๋ฆฌ๋ทฐ์ด ์ค์
- [ ] Assignee ์ค์
- [ ] Label ์ค์
-- [ ] ์ ๋ชฉ ์์ ๋ง์ท๋์? (ex. #0 Feat: ๊ธฐ๋ฅ ์ถ๊ฐ)
-- [ ] ๋ณ๊ฒฝ ์ฌํญ์ ๋ํ ํ
์คํธ
\ No newline at end of file
+- [ ] ์ ๋ชฉ ์์ ๋ง์ท๋์? (ex. [WTH-01] PR ํ
ํ๋ฆฟ ์์ )
+- [ ] ๋ณ๊ฒฝ ์ฌํญ์ ๋ํ ํ
์คํธ
diff --git a/Dockerfile-dev b/Dockerfile-dev
index a9b6b7826..e46cdb889 100644
--- a/Dockerfile-dev
+++ b/Dockerfile-dev
@@ -1,5 +1,5 @@
-# open jdk 17 ๋ฒ์ ์ alpine ๋ฆฌ๋
์ค ํ๊ฒฝ์ ๊ตฌ์ฑ
-FROM openjdk:17-alpine
+# eclipse-temurin 17 ๋ฒ์ ์ alpine ๋ฆฌ๋
์ค ํ๊ฒฝ์ ๊ตฌ์ฑ
+FROM eclipse-temurin:17-jdk-alpine
# build๊ฐ ๋๋ ์์ ์ JAR_FILE์ด๋ผ๋ ๋ณ์ ๋ช
์ build/libs/*.jar ์ ์ธ
# build/libs - gradle๋ก ๋น๋ํ์ ๋ jar ํ์ผ์ด ์์ฑ๋๋ ๊ฒฝ๋ก
diff --git a/Dockerfile-prod b/Dockerfile-prod
index a20969c72..92b747a24 100644
--- a/Dockerfile-prod
+++ b/Dockerfile-prod
@@ -1,5 +1,5 @@
-# open jdk 17 ๋ฒ์ ์ alpine ๋ฆฌ๋
์ค ํ๊ฒฝ์ ๊ตฌ์ฑ
-FROM openjdk:17-alpine
+# eclipse-temurin 17 ๋ฒ์ ์ alpine ๋ฆฌ๋
์ค ํ๊ฒฝ์ ๊ตฌ์ฑ
+FROM eclipse-temurin:17-jdk-alpine
# build๊ฐ ๋๋ ์์ ์ JAR_FILE์ด๋ผ๋ ๋ณ์ ๋ช
์ build/libs/*.jar ์ ์ธ
# build/libs - gradle๋ก ๋น๋ํ์ ๋ jar ํ์ผ์ด ์์ฑ๋๋ ๊ฒฝ๋ก
diff --git a/build.gradle b/build.gradle
index dd370c44d..33b69c6b9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,6 @@
plugins {
id 'java'
- id 'org.springframework.boot' version '3.3.1'
+ id 'org.springframework.boot' version '3.5.7'
id 'io.spring.dependency-management' version '1.1.5'
}
@@ -52,10 +52,15 @@ dependencies {
// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation "org.junit.jupiter:junit-jupiter:5.8.1"
+ testImplementation "org.testcontainers:testcontainers:2.0.1"
+ testImplementation "org.testcontainers:testcontainers-junit-jupiter:2.0.1"
+ testImplementation "org.testcontainers:testcontainers-mysql:2.0.1"
+ testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// Swagger
- implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.14'
// AWS Spring Cloud
implementation 'software.amazon.awssdk:s3:2.19.1'
@@ -66,6 +71,10 @@ dependencies {
// Spring Authorization Server
implementation 'org.springframework.boot:spring-boot-starter-oauth2-authorization-server'
+ // Prometheus
+ implementation 'org.springframework.boot:spring-boot-starter-actuator'
+ runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
+
}
tasks.named('test') {
diff --git a/src/main/java/leets/weeth/domain/attendance/application/usecase/AttendanceUseCaseImpl.java b/src/main/java/leets/weeth/domain/attendance/application/usecase/AttendanceUseCaseImpl.java
index 955172a1c..9840ceac5 100644
--- a/src/main/java/leets/weeth/domain/attendance/application/usecase/AttendanceUseCaseImpl.java
+++ b/src/main/java/leets/weeth/domain/attendance/application/usecase/AttendanceUseCaseImpl.java
@@ -44,7 +44,7 @@ public void checkIn(Long userId, Integer code) throws AttendanceCodeMismatchExce
LocalDateTime now = LocalDateTime.now();
Attendance todayMeeting = user.getAttendances().stream()
- .filter(attendance -> attendance.getMeeting().getStart().isBefore(now)
+ .filter(attendance -> attendance.getMeeting().getStart().minusMinutes(10).isBefore(now)
&& attendance.getMeeting().getEnd().isAfter(now))
.findAny()
.orElseThrow(AttendanceNotFoundException::new);
diff --git a/src/main/java/leets/weeth/domain/board/application/dto/NoticeDTO.java b/src/main/java/leets/weeth/domain/board/application/dto/NoticeDTO.java
index 507b50174..1b56c004e 100644
--- a/src/main/java/leets/weeth/domain/board/application/dto/NoticeDTO.java
+++ b/src/main/java/leets/weeth/domain/board/application/dto/NoticeDTO.java
@@ -1,5 +1,6 @@
package leets.weeth.domain.board.application.dto;
+import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import leets.weeth.domain.comment.application.dto.CommentDTO;
@@ -59,4 +60,11 @@ public record ResponseAll(
) {
}
+ @Builder
+ public record SaveResponse(
+ @Schema(description = "๊ณต์ง์ฌํญ ์์ฑ ์๋ต", example = "1")
+ long id
+ ) {
+ }
+
}
diff --git a/src/main/java/leets/weeth/domain/board/application/dto/PostDTO.java b/src/main/java/leets/weeth/domain/board/application/dto/PostDTO.java
index ced6270ee..bcdc38fb4 100644
--- a/src/main/java/leets/weeth/domain/board/application/dto/PostDTO.java
+++ b/src/main/java/leets/weeth/domain/board/application/dto/PostDTO.java
@@ -1,9 +1,9 @@
package leets.weeth.domain.board.application.dto;
+import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
-import java.time.LocalDateTime;
-import java.util.List;
import leets.weeth.domain.board.domain.entity.enums.Category;
import leets.weeth.domain.board.domain.entity.enums.Part;
import leets.weeth.domain.comment.application.dto.CommentDTO;
@@ -13,19 +13,23 @@
import leets.weeth.domain.user.domain.entity.enums.Role;
import lombok.Builder;
+import java.time.LocalDateTime;
+import java.util.List;
+
public class PostDTO {
@Builder
public record Save(
- @NotNull String title,
- @NotNull String content,
+ @NotBlank(message = "์ ๋ชฉ ์
๋ ฅ์ ํ์์
๋๋ค.") String title,
+ @NotBlank(message = "๋ด์ฉ ์
๋ ฅ์ ํ์์
๋๋ค.") String content,
@NotNull Category category,
String studyName,
int week,
@NotNull Part part,
@NotNull Integer cardinalNumber,
@Valid List<@NotNull FileSaveRequest> files
- ){}
+ ) {
+ }
@Builder
public record SaveEducation(
@@ -34,7 +38,15 @@ public record SaveEducation(
@NotNull List parts,
@NotNull Integer cardinalNumber,
@Valid List<@NotNull FileSaveRequest> files
- ){}
+ ) {
+ }
+
+ @Builder
+ public record SaveResponse(
+ @Schema(description = "๊ฒ์๊ธ ์์ฑ์ ์๋ต", example = "1")
+ long id
+ ) {
+ }
@Builder
public record Update(
@@ -45,7 +57,8 @@ public record Update(
Part part,
Integer cardinalNumber,
@Valid List files
- ){}
+ ) {
+ }
@Builder
public record UpdateEducation(
@@ -54,7 +67,8 @@ public record UpdateEducation(
List parts,
Integer cardinalNumber,
@Valid List files
- ){}
+ ) {
+ }
@Builder
public record Response(
@@ -73,7 +87,8 @@ public record Response(
Integer commentCount,
List comments,
List fileUrls
- ){}
+ ) {
+ }
@Builder
public record ResponseAll(
@@ -90,7 +105,8 @@ public record ResponseAll(
Integer commentCount,
boolean hasFile,
boolean isNew
- ){}
+ ) {
+ }
@Builder
public record ResponseEducationAll(
@@ -105,9 +121,11 @@ public record ResponseEducationAll(
Integer commentCount,
boolean hasFile,
boolean isNew
- ){}
+ ) {
+ }
public record ResponseStudyNames(
List studyNames
- ) {}
+ ) {
+ }
}
diff --git a/src/main/java/leets/weeth/domain/board/application/mapper/NoticeMapper.java b/src/main/java/leets/weeth/domain/board/application/mapper/NoticeMapper.java
index fd39cffbd..9509e24ae 100644
--- a/src/main/java/leets/weeth/domain/board/application/mapper/NoticeMapper.java
+++ b/src/main/java/leets/weeth/domain/board/application/mapper/NoticeMapper.java
@@ -37,4 +37,6 @@ public interface NoticeMapper {
})
NoticeDTO.Response toNoticeDto(Notice notice, List fileUrls, List comments);
+ NoticeDTO.SaveResponse toSaveResponse(Notice notice);
+
}
diff --git a/src/main/java/leets/weeth/domain/board/application/mapper/PostMapper.java b/src/main/java/leets/weeth/domain/board/application/mapper/PostMapper.java
index 95b199095..1bbade147 100644
--- a/src/main/java/leets/weeth/domain/board/application/mapper/PostMapper.java
+++ b/src/main/java/leets/weeth/domain/board/application/mapper/PostMapper.java
@@ -34,6 +34,8 @@ public interface PostMapper {
@Mapping(target = "category", constant = "Education")
Post fromEducationDto(PostDTO.SaveEducation dto, User user);
+ PostDTO.SaveResponse toSaveResponse(Post post);
+
@Mappings({
@Mapping(target = "name", source = "post.user.name"),
@Mapping(target = "position", source = "post.user.position"),
diff --git a/src/main/java/leets/weeth/domain/board/application/usecase/NoticeUsecase.java b/src/main/java/leets/weeth/domain/board/application/usecase/NoticeUsecase.java
index 00f20557d..708e20615 100644
--- a/src/main/java/leets/weeth/domain/board/application/usecase/NoticeUsecase.java
+++ b/src/main/java/leets/weeth/domain/board/application/usecase/NoticeUsecase.java
@@ -6,14 +6,13 @@
public interface NoticeUsecase {
-
- void save(NoticeDTO.Save dto, Long userId);
+ NoticeDTO.SaveResponse save(NoticeDTO.Save dto, Long userId);
NoticeDTO.Response findNotice(Long noticeId);
Slice findNotices(int pageNumber, int pageSize);
- void update(Long noticeId, NoticeDTO.Update dto, Long userId) throws UserNotMatchException;
+ NoticeDTO.SaveResponse update(Long noticeId, NoticeDTO.Update dto, Long userId) throws UserNotMatchException;
void delete(Long noticeId, Long userId) throws UserNotMatchException;
diff --git a/src/main/java/leets/weeth/domain/board/application/usecase/NoticeUsecaseImpl.java b/src/main/java/leets/weeth/domain/board/application/usecase/NoticeUsecaseImpl.java
index a04c3bc83..bcbe76acb 100644
--- a/src/main/java/leets/weeth/domain/board/application/usecase/NoticeUsecaseImpl.java
+++ b/src/main/java/leets/weeth/domain/board/application/usecase/NoticeUsecaseImpl.java
@@ -1,9 +1,5 @@
package leets.weeth.domain.board.application.usecase;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
import leets.weeth.domain.board.application.dto.NoticeDTO;
import leets.weeth.domain.board.application.exception.NoSearchResultException;
import leets.weeth.domain.board.application.exception.PageNotFoundException;
@@ -33,6 +29,11 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
@Service
@RequiredArgsConstructor
public class NoticeUsecaseImpl implements NoticeUsecase {
@@ -54,14 +55,16 @@ public class NoticeUsecaseImpl implements NoticeUsecase {
@Override
@Transactional
- public void save(NoticeDTO.Save request, Long userId) {
+ public NoticeDTO.SaveResponse save(NoticeDTO.Save request, Long userId) {
User user = userGetService.find(userId);
Notice notice = mapper.fromNoticeDto(request, user);
- noticeSaveService.save(notice);
+ Notice savedNotice = noticeSaveService.save(notice);
List files = fileMapper.toFileList(request.files(), notice);
fileSaveService.save(files);
+
+ return mapper.toSaveResponse(savedNotice);
}
@Override
@@ -103,7 +106,7 @@ public Slice searchNotice(String keyword, int pageNumber,
@Override
@Transactional
- public void update(Long noticeId, NoticeDTO.Update dto, Long userId) {
+ public NoticeDTO.SaveResponse update(Long noticeId, NoticeDTO.Update dto, Long userId) {
Notice notice = validateOwner(noticeId, userId);
List fileList = getFiles(noticeId);
@@ -113,6 +116,8 @@ public void update(Long noticeId, NoticeDTO.Update dto, Long userId) {
fileSaveService.save(files);
noticeUpdateService.update(notice, dto);
+
+ return mapper.toSaveResponse(notice);
}
@Override
diff --git a/src/main/java/leets/weeth/domain/board/application/usecase/PostUseCaseImpl.java b/src/main/java/leets/weeth/domain/board/application/usecase/PostUseCaseImpl.java
index 1ad0e60ea..b213258c6 100644
--- a/src/main/java/leets/weeth/domain/board/application/usecase/PostUseCaseImpl.java
+++ b/src/main/java/leets/weeth/domain/board/application/usecase/PostUseCaseImpl.java
@@ -1,9 +1,5 @@
package leets.weeth.domain.board.application.usecase;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
import leets.weeth.domain.board.application.dto.PartPostDTO;
import leets.weeth.domain.board.application.dto.PostDTO;
import leets.weeth.domain.board.application.exception.CategoryAccessDeniedException;
@@ -33,14 +29,15 @@
import leets.weeth.domain.user.domain.service.UserCardinalGetService;
import leets.weeth.domain.user.domain.service.UserGetService;
import lombok.RequiredArgsConstructor;
-import org.springframework.data.domain.PageRequest;
-import org.springframework.data.domain.Pageable;
-import org.springframework.data.domain.Slice;
-import org.springframework.data.domain.SliceImpl;
-import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
@Service
@RequiredArgsConstructor
public class PostUseCaseImpl implements PostUsecase {
@@ -64,7 +61,7 @@ public class PostUseCaseImpl implements PostUsecase {
@Override
@Transactional
- public void save(PostDTO.Save request, Long userId) {
+ public PostDTO.SaveResponse save(PostDTO.Save request, Long userId) {
User user = userGetService.find(userId);
if (request.category() == Category.Education
@@ -74,23 +71,26 @@ public void save(PostDTO.Save request, Long userId) {
cardinalGetService.findByUserSide(request.cardinalNumber());
Post post = mapper.fromPostDto(request, user);
- postSaveService.save(post);
+ Post savedPost = postSaveService.save(post);
List files = fileMapper.toFileList(request.files(), post);
fileSaveService.save(files);
+
+ return mapper.toSaveResponse(savedPost);
}
@Override
@Transactional
- public void saveEducation(PostDTO.SaveEducation request, Long userId) {
+ public PostDTO.SaveResponse saveEducation(PostDTO.SaveEducation request, Long userId) {
User user = userGetService.find(userId);
Post post = mapper.fromEducationDto(request, user);
-
- postSaveService.save(post);
+ Post saverPost = postSaveService.save(post);
List files = fileMapper.toFileList(request.files(), post);
fileSaveService.save(files);
+
+ return mapper.toSaveResponse(saverPost);
}
@Override
@@ -193,7 +193,7 @@ public Slice searchEducation(String keyword, int p
@Override
@Transactional
- public void update(Long postId, PostDTO.Update dto, Long userId) {
+ public PostDTO.SaveResponse update(Long postId, PostDTO.Update dto, Long userId) {
Post post = validateOwner(postId, userId);
if (dto.files() != null) {
@@ -205,11 +205,13 @@ public void update(Long postId, PostDTO.Update dto, Long userId) {
}
postUpdateService.update(post, dto);
+
+ return mapper.toSaveResponse(post);
}
@Override
@Transactional
- public void updateEducation(Long postId, PostDTO.UpdateEducation dto, Long userId) {
+ public PostDTO.SaveResponse updateEducation(Long postId, PostDTO.UpdateEducation dto, Long userId) {
Post post = validateOwner(postId, userId);
if (dto.files() != null) {
@@ -221,6 +223,8 @@ public void updateEducation(Long postId, PostDTO.UpdateEducation dto, Long userI
}
postUpdateService.updateEducation(post, dto);
+
+ return mapper.toSaveResponse(post);
}
@Override
diff --git a/src/main/java/leets/weeth/domain/board/application/usecase/PostUsecase.java b/src/main/java/leets/weeth/domain/board/application/usecase/PostUsecase.java
index c2acb2e43..4755a26ca 100644
--- a/src/main/java/leets/weeth/domain/board/application/usecase/PostUsecase.java
+++ b/src/main/java/leets/weeth/domain/board/application/usecase/PostUsecase.java
@@ -9,9 +9,9 @@
public interface PostUsecase {
- void save(PostDTO.Save request, Long userId);
+ PostDTO.SaveResponse save(PostDTO.Save request, Long userId);
- void saveEducation(PostDTO.SaveEducation request, Long userId);
+ PostDTO.SaveResponse saveEducation(PostDTO.SaveEducation request, Long userId);
PostDTO.Response findPost(Long postId);
@@ -23,9 +23,9 @@ public interface PostUsecase {
PostDTO.ResponseStudyNames findStudyNames(Part part);
- void update(Long postId, PostDTO.Update dto, Long userId) throws UserNotMatchException;
+ PostDTO.SaveResponse update(Long postId, PostDTO.Update dto, Long userId) throws UserNotMatchException;
- void updateEducation(Long postId, PostDTO.UpdateEducation dto, Long userId) throws UserNotMatchException;
+ PostDTO.SaveResponse updateEducation(Long postId, PostDTO.UpdateEducation dto, Long userId) throws UserNotMatchException;
void delete(Long postId, Long userId) throws UserNotMatchException;
diff --git a/src/main/java/leets/weeth/domain/board/domain/service/NoticeSaveService.java b/src/main/java/leets/weeth/domain/board/domain/service/NoticeSaveService.java
index b26e1061b..30dbcfbfa 100644
--- a/src/main/java/leets/weeth/domain/board/domain/service/NoticeSaveService.java
+++ b/src/main/java/leets/weeth/domain/board/domain/service/NoticeSaveService.java
@@ -1,6 +1,5 @@
package leets.weeth.domain.board.domain.service;
-import jakarta.transaction.Transactional;
import leets.weeth.domain.board.domain.entity.Notice;
import leets.weeth.domain.board.domain.repository.NoticeRepository;
import lombok.RequiredArgsConstructor;
@@ -12,8 +11,8 @@ public class NoticeSaveService {
private final NoticeRepository noticeRepository;
- public void save(Notice notice){
- noticeRepository.save(notice);
+ public Notice save(Notice notice){
+ return noticeRepository.save(notice);
}
}
diff --git a/src/main/java/leets/weeth/domain/board/domain/service/PostSaveService.java b/src/main/java/leets/weeth/domain/board/domain/service/PostSaveService.java
index 80cd5b555..c427b3c3f 100644
--- a/src/main/java/leets/weeth/domain/board/domain/service/PostSaveService.java
+++ b/src/main/java/leets/weeth/domain/board/domain/service/PostSaveService.java
@@ -1,6 +1,5 @@
package leets.weeth.domain.board.domain.service;
-import jakarta.transaction.Transactional;
import leets.weeth.domain.board.domain.entity.Post;
import leets.weeth.domain.board.domain.repository.PostRepository;
import lombok.RequiredArgsConstructor;
@@ -12,8 +11,7 @@ public class PostSaveService {
private final PostRepository postRepository;
- public void save(Post post) {
- postRepository.save(post);
+ public Post save(Post post) {
+ return postRepository.save(post);
}
-
}
diff --git a/src/main/java/leets/weeth/domain/board/presentation/EducationAdminController.java b/src/main/java/leets/weeth/domain/board/presentation/EducationAdminController.java
index 106eaa6c1..196bc8cf2 100644
--- a/src/main/java/leets/weeth/domain/board/presentation/EducationAdminController.java
+++ b/src/main/java/leets/weeth/domain/board/presentation/EducationAdminController.java
@@ -1,8 +1,5 @@
package leets.weeth.domain.board.presentation;
-import static leets.weeth.domain.board.presentation.ResponseMessage.EDUCATION_UPDATED_SUCCESS;
-import static leets.weeth.domain.board.presentation.ResponseMessage.POST_CREATED_SUCCESS;
-
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -13,12 +10,10 @@
import leets.weeth.global.auth.annotation.CurrentUser;
import leets.weeth.global.common.response.CommonResponse;
import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.PatchMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+
+import static leets.weeth.domain.board.presentation.ResponseMessage.EDUCATION_UPDATED_SUCCESS;
+import static leets.weeth.domain.board.presentation.ResponseMessage.POST_CREATED_SUCCESS;
@Tag(name = "EDUCATION ADMIN", description = "[ADMIN] ๊ณต์ง์ฌํญ ๊ต์ก์๋ฃ API")
@RestController
@@ -29,18 +24,19 @@ public class EducationAdminController {
@PostMapping("/education")
@Operation(summary = "๊ต์ก์๋ฃ ์์ฑ")
- public CommonResponse saveEducation(@RequestBody @Valid PostDTO.SaveEducation dto, @Parameter(hidden = true) @CurrentUser Long userId) {
- postUsecase.saveEducation(dto, userId);
+ public CommonResponse saveEducation(@RequestBody @Valid PostDTO.SaveEducation dto, @Parameter(hidden = true) @CurrentUser Long userId) {
+ PostDTO.SaveResponse response = postUsecase.saveEducation(dto, userId);
- return CommonResponse.createSuccess(POST_CREATED_SUCCESS.getMessage());
+ return CommonResponse.createSuccess(POST_CREATED_SUCCESS.getMessage(), response);
}
@PatchMapping(value = "/{boardId}")
@Operation(summary="๊ต์ก์๋ฃ ๊ฒ์๊ธ ์์ ")
- public CommonResponse update(@PathVariable Long boardId,
+ public CommonResponse update(@PathVariable Long boardId,
@RequestBody @Valid PostDTO.UpdateEducation dto,
@Parameter(hidden = true) @CurrentUser Long userId) throws UserNotMatchException {
- postUsecase.updateEducation(boardId, dto, userId);
- return CommonResponse.createSuccess(EDUCATION_UPDATED_SUCCESS.getMessage());
+ PostDTO.SaveResponse response = postUsecase.updateEducation(boardId, dto, userId);
+
+ return CommonResponse.createSuccess(EDUCATION_UPDATED_SUCCESS.getMessage(), response);
}
}
diff --git a/src/main/java/leets/weeth/domain/board/presentation/NoticeAdminController.java b/src/main/java/leets/weeth/domain/board/presentation/NoticeAdminController.java
index 28f4bcc95..520736d60 100644
--- a/src/main/java/leets/weeth/domain/board/presentation/NoticeAdminController.java
+++ b/src/main/java/leets/weeth/domain/board/presentation/NoticeAdminController.java
@@ -6,15 +6,11 @@
import jakarta.validation.Valid;
import leets.weeth.domain.board.application.dto.NoticeDTO;
import leets.weeth.domain.board.application.usecase.NoticeUsecase;
-import leets.weeth.global.auth.annotation.CurrentUser;
import leets.weeth.domain.user.application.exception.UserNotMatchException;
+import leets.weeth.global.auth.annotation.CurrentUser;
import leets.weeth.global.common.response.CommonResponse;
import lombok.RequiredArgsConstructor;
-import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
-import org.springframework.web.multipart.MultipartFile;
-
-import java.util.List;
import static leets.weeth.domain.board.presentation.ResponseMessage.*;
@@ -28,19 +24,21 @@ public class NoticeAdminController {
@PostMapping
@Operation(summary="๊ณต์ง์ฌํญ ์์ฑ")
- public CommonResponse save(@RequestBody @Valid NoticeDTO.Save dto,
+ public CommonResponse save(@RequestBody @Valid NoticeDTO.Save dto,
@Parameter(hidden = true) @CurrentUser Long userId) {
- noticeUsecase.save(dto, userId);
- return CommonResponse.createSuccess(NOTICE_CREATED_SUCCESS.getMessage());
+ NoticeDTO.SaveResponse response = noticeUsecase.save(dto, userId);
+
+ return CommonResponse.createSuccess(NOTICE_CREATED_SUCCESS.getMessage(), response);
}
@PatchMapping(value = "/{noticeId}")
@Operation(summary="ํน์ ๊ณต์ง์ฌํญ ์์ ")
- public CommonResponse update(@PathVariable Long noticeId,
+ public CommonResponse update(@PathVariable Long noticeId,
@RequestBody @Valid NoticeDTO.Update dto,
@Parameter(hidden = true) @CurrentUser Long userId) throws UserNotMatchException {
- noticeUsecase.update(noticeId, dto, userId);
- return CommonResponse.createSuccess(NOTICE_UPDATED_SUCCESS.getMessage());
+ NoticeDTO.SaveResponse response = noticeUsecase.update(noticeId, dto, userId);
+
+ return CommonResponse.createSuccess(NOTICE_UPDATED_SUCCESS.getMessage(), response);
}
@DeleteMapping("/{noticeId}")
diff --git a/src/main/java/leets/weeth/domain/board/presentation/PostController.java b/src/main/java/leets/weeth/domain/board/presentation/PostController.java
index 05714ed63..6bfbf45be 100644
--- a/src/main/java/leets/weeth/domain/board/presentation/PostController.java
+++ b/src/main/java/leets/weeth/domain/board/presentation/PostController.java
@@ -1,15 +1,5 @@
package leets.weeth.domain.board.presentation;
-import static leets.weeth.domain.board.presentation.ResponseMessage.EDUCATION_SEARCH_SUCCESS;
-import static leets.weeth.domain.board.presentation.ResponseMessage.POST_CREATED_SUCCESS;
-import static leets.weeth.domain.board.presentation.ResponseMessage.POST_DELETED_SUCCESS;
-import static leets.weeth.domain.board.presentation.ResponseMessage.POST_EDU_FIND_SUCCESS;
-import static leets.weeth.domain.board.presentation.ResponseMessage.POST_FIND_ALL_SUCCESS;
-import static leets.weeth.domain.board.presentation.ResponseMessage.POST_FIND_BY_ID_SUCCESS;
-import static leets.weeth.domain.board.presentation.ResponseMessage.POST_PART_FIND_ALL_SUCCESS;
-import static leets.weeth.domain.board.presentation.ResponseMessage.POST_SEARCH_SUCCESS;
-import static leets.weeth.domain.board.presentation.ResponseMessage.POST_UPDATED_SUCCESS;
-
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -23,16 +13,9 @@
import leets.weeth.global.common.response.CommonResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Slice;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.ModelAttribute;
-import org.springframework.web.bind.annotation.PatchMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+
+import static leets.weeth.domain.board.presentation.ResponseMessage.*;
@Tag(name = "BOARD", description = "๊ฒ์ํ API")
@RestController
@@ -44,10 +27,10 @@ public class PostController {
@PostMapping
@Operation(summary="ํํธ ๊ฒ์๊ธ ์์ฑ (์คํฐ๋ ๋ก๊ทธ, ์ํฐํด)")
- public CommonResponse save(@RequestBody @Valid PostDTO.Save dto, @Parameter(hidden = true) @CurrentUser Long userId) {
- postUsecase.save(dto, userId);
+ public CommonResponse save(@RequestBody @Valid PostDTO.Save dto, @Parameter(hidden = true) @CurrentUser Long userId) {
+ PostDTO.SaveResponse response = postUsecase.save(dto, userId);
- return CommonResponse.createSuccess(POST_CREATED_SUCCESS.getMessage());
+ return CommonResponse.createSuccess(POST_CREATED_SUCCESS.getMessage(), response);
}
@GetMapping
@@ -101,11 +84,12 @@ public CommonResponse> findEducation(@Reques
@PatchMapping(value = "/{boardId}/part")
@Operation(summary="ํํธ ๊ฒ์๊ธ ์์ ")
- public CommonResponse update(@PathVariable Long boardId,
+ public CommonResponse update(@PathVariable Long boardId,
@RequestBody @Valid PostDTO.Update dto,
@Parameter(hidden = true) @CurrentUser Long userId) throws UserNotMatchException {
- postUsecase.update(boardId, dto, userId);
- return CommonResponse.createSuccess(POST_UPDATED_SUCCESS.getMessage());
+ PostDTO.SaveResponse response = postUsecase.update(boardId, dto, userId);
+
+ return CommonResponse.createSuccess(POST_UPDATED_SUCCESS.getMessage(), response);
}
@DeleteMapping("/{boardId}")
diff --git a/src/main/java/leets/weeth/domain/comment/application/dto/CommentDTO.java b/src/main/java/leets/weeth/domain/comment/application/dto/CommentDTO.java
index 639e377d1..25d124c32 100644
--- a/src/main/java/leets/weeth/domain/comment/application/dto/CommentDTO.java
+++ b/src/main/java/leets/weeth/domain/comment/application/dto/CommentDTO.java
@@ -3,6 +3,7 @@
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
import leets.weeth.domain.file.application.dto.request.FileSaveRequest;
import leets.weeth.domain.file.application.dto.response.FileResponse;
import leets.weeth.domain.user.domain.entity.enums.Position;
@@ -17,13 +18,13 @@ public class CommentDTO {
@Builder
public record Save(
Long parentCommentId,
- @NotBlank String content,
+ @NotBlank @Size(max=300, message = "๋๊ธ์ ์ต๋ 300์๊น์ง ๊ฐ๋ฅํฉ๋๋ค.") String content,
@Valid List<@NotNull FileSaveRequest> files
){}
@Builder
public record Update(
- @NotBlank String content,
+ @NotBlank @Size(max=300, message = "๋๊ธ์ ์ต๋ 300์๊น์ง ๊ฐ๋ฅํฉ๋๋ค.") String content,
@Valid List<@NotNull FileSaveRequest> files
){}
diff --git a/src/main/java/leets/weeth/domain/comment/domain/entity/Comment.java b/src/main/java/leets/weeth/domain/comment/domain/entity/Comment.java
index db791dbb2..dde416246 100644
--- a/src/main/java/leets/weeth/domain/comment/domain/entity/Comment.java
+++ b/src/main/java/leets/weeth/domain/comment/domain/entity/Comment.java
@@ -27,6 +27,7 @@ public class Comment extends BaseEntity {
@Column(name = "comment_id")
private Long id;
+ @Column(length = 300)
private String content;
@Column(nullable = false)
diff --git a/src/main/java/leets/weeth/domain/schedule/application/usecase/MeetingUseCaseImpl.java b/src/main/java/leets/weeth/domain/schedule/application/usecase/MeetingUseCaseImpl.java
index ab80aea31..8889a49b7 100644
--- a/src/main/java/leets/weeth/domain/schedule/application/usecase/MeetingUseCaseImpl.java
+++ b/src/main/java/leets/weeth/domain/schedule/application/usecase/MeetingUseCaseImpl.java
@@ -1,5 +1,7 @@
package leets.weeth.domain.schedule.application.usecase;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
import leets.weeth.domain.attendance.domain.entity.Attendance;
import leets.weeth.domain.attendance.domain.service.AttendanceDeleteService;
import leets.weeth.domain.attendance.domain.service.AttendanceGetService;
@@ -44,6 +46,9 @@ public class MeetingUseCaseImpl implements MeetingUseCase {
private final AttendanceUpdateService attendanceUpdateService;
private final CardinalGetService cardinalGetService;
+ @PersistenceContext
+ private EntityManager em;
+
@Override
public Response find(Long userId, Long meetingId) {
User user = userGetService.find(userId);
@@ -103,6 +108,9 @@ public void delete(Long meetingId) {
attendanceUpdateService.updateUserAttendanceByStatus(attendances);
+ em.flush();
+ em.clear();
+
attendanceDeleteService.deleteAll(meeting);
meetingDeleteService.delete(meeting);
}
diff --git a/src/main/java/leets/weeth/domain/user/application/dto/request/UserRequestDto.java b/src/main/java/leets/weeth/domain/user/application/dto/request/UserRequestDto.java
index 2cfdb8cf5..30ac0336b 100644
--- a/src/main/java/leets/weeth/domain/user/application/dto/request/UserRequestDto.java
+++ b/src/main/java/leets/weeth/domain/user/application/dto/request/UserRequestDto.java
@@ -1,5 +1,6 @@
package leets.weeth.domain.user.application.dto.request;
+import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@@ -27,7 +28,10 @@ public record SignUp(
}
public record Register(
- @NotNull Long kakaoId,
+ @Schema(description = "kakao๋ก ํ์๊ฐ์
ํ๋ ๊ฒฝ์ฐ")
+ Long kakaoId,
+ @Schema(description = "์ ํ๋ก ํ์๊ฐ์
ํ๋ ๊ฒฝ์ฐ - Apple OAuth authCode")
+ String appleAuthCode,
@NotBlank String name,
@NotBlank String studentId,
@NotBlank String email,
diff --git a/src/main/java/leets/weeth/domain/user/application/dto/response/UserResponseDto.java b/src/main/java/leets/weeth/domain/user/application/dto/response/UserResponseDto.java
index 8f38c0ed5..5a76d6456 100644
--- a/src/main/java/leets/weeth/domain/user/application/dto/response/UserResponseDto.java
+++ b/src/main/java/leets/weeth/domain/user/application/dto/response/UserResponseDto.java
@@ -13,6 +13,7 @@ public class UserResponseDto {
public record SocialLoginResponse(
Long id,
Long kakaoId,
+ String appleIdToken,
LoginStatus status,
String accessToken,
String refreshToken
diff --git a/src/main/java/leets/weeth/domain/user/application/mapper/UserMapper.java b/src/main/java/leets/weeth/domain/user/application/mapper/UserMapper.java
index e41866ecd..046a0f5f2 100644
--- a/src/main/java/leets/weeth/domain/user/application/mapper/UserMapper.java
+++ b/src/main/java/leets/weeth/domain/user/application/mapper/UserMapper.java
@@ -49,11 +49,15 @@ public interface UserMapper {
@Mapping(target = "status", expression = "java(LoginStatus.LOGIN)"),
@Mapping(target = "id", source = "user.id"),
@Mapping(target = "kakaoId", source = "user.kakaoId"),
+ @Mapping(target = "appleIdToken", expression = "java(null)")
})
SocialLoginResponse toLoginResponse(User user, JwtDto dto);
@Mappings({
@Mapping(target = "status", expression = "java(LoginStatus.INTEGRATE)"),
+ @Mapping(target = "appleIdToken", expression = "java(null)"),
+ @Mapping(target = "accessToken", expression = "java(null)"),
+ @Mapping(target = "refreshToken", expression = "java(null)")
})
SocialLoginResponse toIntegrateResponse(Long kakaoId);
@@ -66,6 +70,24 @@ public interface UserMapper {
@Mapping(target = "cardinals", expression = "java( toCardinalNumbers(userCardinals) )")
UserResponseDto.UserInfo toUserInfoDto(User user, List userCardinals);
+ @Mappings({
+ @Mapping(target = "status", expression = "java(LoginStatus.LOGIN)"),
+ @Mapping(target = "id", source = "user.id"),
+ @Mapping(target = "appleIdToken", expression = "java(null)"),
+ @Mapping(target = "kakaoId", expression = "java(null)")
+ })
+ SocialLoginResponse toAppleLoginResponse(User user, JwtDto dto);
+
+ @Mappings({
+ @Mapping(target = "status", expression = "java(LoginStatus.INTEGRATE)"),
+ @Mapping(target = "id", expression = "java(null)"),
+ @Mapping(target = "appleIdToken", source = "appleIdToken"),
+ @Mapping(target = "kakaoId", expression = "java(null)"),
+ @Mapping(target = "accessToken", expression = "java(null)"),
+ @Mapping(target = "refreshToken", expression = "java(null)")
+ })
+ SocialLoginResponse toAppleIntegrateResponse(String appleIdToken);
+
default String toString(Department department) {
return department.getValue();
}
diff --git a/src/main/java/leets/weeth/domain/user/application/usecase/UserUseCase.java b/src/main/java/leets/weeth/domain/user/application/usecase/UserUseCase.java
index ae71bf995..e1164291f 100644
--- a/src/main/java/leets/weeth/domain/user/application/usecase/UserUseCase.java
+++ b/src/main/java/leets/weeth/domain/user/application/usecase/UserUseCase.java
@@ -37,4 +37,8 @@ public interface UserUseCase {
List searchUser(String keyword);
+ SocialLoginResponse appleLogin(Login dto);
+
+ void appleRegister(Register dto);
+
}
diff --git a/src/main/java/leets/weeth/domain/user/application/usecase/UserUseCaseImpl.java b/src/main/java/leets/weeth/domain/user/application/usecase/UserUseCaseImpl.java
index 2cbe3ecdf..5a4ddab73 100644
--- a/src/main/java/leets/weeth/domain/user/application/usecase/UserUseCaseImpl.java
+++ b/src/main/java/leets/weeth/domain/user/application/usecase/UserUseCaseImpl.java
@@ -11,6 +11,8 @@
import leets.weeth.domain.user.domain.entity.User;
import leets.weeth.domain.user.domain.entity.UserCardinal;
import leets.weeth.domain.user.domain.service.*;
+import leets.weeth.global.auth.apple.dto.AppleTokenResponse;
+import leets.weeth.global.auth.apple.dto.AppleUserInfo;
import leets.weeth.global.auth.jwt.application.dto.JwtDto;
import leets.weeth.global.auth.jwt.application.usecase.JwtManageUseCase;
import leets.weeth.global.auth.kakao.KakaoAuthService;
@@ -18,6 +20,7 @@
import leets.weeth.global.auth.kakao.dto.KakaoUserInfoResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.env.Environment;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
@@ -25,10 +28,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
import java.util.stream.Collectors;
import static leets.weeth.domain.user.application.dto.request.UserRequestDto.*;
@@ -44,6 +44,7 @@ public class UserUseCaseImpl implements UserUseCase {
private final UserGetService userGetService;
private final UserUpdateService userUpdateService;
private final KakaoAuthService kakaoAuthService;
+ private final leets.weeth.global.auth.apple.AppleAuthService appleAuthService;
private final CardinalGetService cardinalGetService;
private final UserCardinalSaveService userCardinalSaveService;
private final UserCardinalGetService userCardinalGetService;
@@ -51,6 +52,7 @@ public class UserUseCaseImpl implements UserUseCase {
private final UserMapper mapper;
private final CardinalMapper cardinalMapper;
private final PasswordEncoder passwordEncoder;
+ private final Environment environment;
@Override
@Transactional(readOnly = true)
@@ -238,4 +240,75 @@ private UserCardinalDto getUserCardinalDto(Long userId) {
return cardinalMapper.toUserCardinalDto(user, userCardinals);
}
+
+ @Override
+ @Transactional(readOnly = true)
+ public SocialLoginResponse appleLogin(Login dto) {
+ // Apple Token ์์ฒญ ๋ฐ ์ ์ ์ ๋ณด ์์ฒญ
+ AppleTokenResponse tokenResponse = appleAuthService.getAppleToken(dto.authCode());
+ AppleUserInfo userInfo = appleAuthService.verifyAndDecodeIdToken(tokenResponse.id_token());
+
+ String appleIdToken = tokenResponse.id_token();
+ String appleId = userInfo.appleId();
+
+ Optional optionalUser = userGetService.findByAppleId(appleId);
+
+ //todo: ์ถํ ์ ํ ๋ก๊ทธ์ธ ์ฐ๋์ ์ํด appleIdToken์ ๋ฐํ
+ // ์ ํ ๋ก๊ทธ์ธ ์ฐ๋ API ์์ฒญ์ appleIdToken์ ํจ๊ป ๋ฃ์ด์ฃผ๋ฉด ๊ทธ๋ ๋์ฝ๋ฉํด์ appleId๋ฅผ ์ถ์ถ
+ if (optionalUser.isEmpty()) {
+ return mapper.toAppleIntegrateResponse(appleIdToken);
+ }
+
+ User user = optionalUser.get();
+ if (user.isInactive()) {
+ throw new UserInActiveException();
+ }
+
+ JwtDto token = jwtManageUseCase.create(user.getId(), user.getEmail(), user.getRole());
+ return mapper.toAppleLoginResponse(user, token);
+ }
+
+ @Override
+ @Transactional
+ public void appleRegister(Register dto) {
+ validate(dto);
+
+ // Apple authCode๋ก ํ ํฐ ๊ตํ ํ ID Token ๊ฒ์ฆ ๋ฐ ์ฌ์ฉ์ ์ ๋ณด ์ถ์ถ
+ AppleTokenResponse tokenResponse = appleAuthService.getAppleToken(dto.appleAuthCode());
+ AppleUserInfo appleUserInfo = appleAuthService.verifyAndDecodeIdToken(tokenResponse.id_token());
+
+ Cardinal cardinal = cardinalGetService.findByUserSide(dto.cardinal());
+
+ User user = mapper.from(dto);
+ // Apple ID ์ค์
+ user.addAppleId(appleUserInfo.appleId());
+
+ UserCardinal userCardinal = new UserCardinal(user, cardinal);
+
+ userSaveService.save(user);
+ userCardinalSaveService.save(userCardinal);
+
+ // dev ํ๊ฒฝ์์๋ง ๋ฐ๋ก ACTIVE ์ํ๋ก ์ค์
+ if (isDevEnvironment()) {
+ log.info("dev ํ๊ฒฝ ๊ฐ์ง: ์ฌ์ฉ์ ์๋ ์น์ธ ์ฒ๋ฆฌ (userId: {})", user.getId());
+ user.accept();
+ }
+ }
+
+ /**
+ * ํ์ฌ ํ๊ฒฝ์ด dev ํ๋กํ์ผ์ธ์ง ํ์ธ
+ * @return dev ํ๋กํ์ผ์ด ํ์ฑํ๋์ด ์์ผ๋ฉด true
+ */
+ private boolean isDevEnvironment() {
+ String[] activeProfiles = environment.getActiveProfiles();
+ for (String profile : activeProfiles) {
+ if ("dev".equals(profile)) {
+ return true;
+ }
+ if ("local".equals(profile)) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/src/main/java/leets/weeth/domain/user/domain/entity/User.java b/src/main/java/leets/weeth/domain/user/domain/entity/User.java
index 22e020bed..9b5a137e9 100644
--- a/src/main/java/leets/weeth/domain/user/domain/entity/User.java
+++ b/src/main/java/leets/weeth/domain/user/domain/entity/User.java
@@ -1,20 +1,6 @@
package leets.weeth.domain.user.domain.entity;
-import static leets.weeth.domain.user.application.dto.request.UserRequestDto.Update;
-
-import jakarta.persistence.CascadeType;
-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.OneToMany;
-import jakarta.persistence.PrePersist;
-import jakarta.persistence.Table;
-import java.util.ArrayList;
-import java.util.List;
+import jakarta.persistence.*;
import leets.weeth.domain.attendance.domain.entity.Attendance;
import leets.weeth.domain.board.domain.entity.enums.Part;
import leets.weeth.domain.user.domain.entity.enums.Department;
@@ -29,6 +15,11 @@
import lombok.experimental.SuperBuilder;
import org.springframework.security.crypto.password.PasswordEncoder;
+import java.util.ArrayList;
+import java.util.List;
+
+import static leets.weeth.domain.user.application.dto.request.UserRequestDto.Update;
+
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@@ -42,8 +33,12 @@ public class User extends BaseEntity {
@Column(name = "user_id")
private Long id;
+ @Column(unique = true)
private Long kakaoId;
+ @Column(unique = true)
+ private String appleId;
+
private String name;
private String email;
@@ -94,6 +89,10 @@ public void addKakaoId(long kakaoId) {
this.kakaoId = kakaoId;
}
+ public void addAppleId(String appleId) {
+ this.appleId = appleId;
+ }
+
public void leave() {
this.status = Status.LEFT;
}
diff --git a/src/main/java/leets/weeth/domain/user/domain/repository/UserRepository.java b/src/main/java/leets/weeth/domain/user/domain/repository/UserRepository.java
index 23c61e5a2..8881f0a4e 100644
--- a/src/main/java/leets/weeth/domain/user/domain/repository/UserRepository.java
+++ b/src/main/java/leets/weeth/domain/user/domain/repository/UserRepository.java
@@ -18,6 +18,8 @@ public interface UserRepository extends JpaRepository {
Optional findByKakaoId(long kakaoId);
+ Optional findByAppleId(String appleId);
+
ListfindAllByNameContainingAndStatus(String name, Status status);
boolean existsByEmail(String email);
diff --git a/src/main/java/leets/weeth/domain/user/domain/service/UserGetService.java b/src/main/java/leets/weeth/domain/user/domain/service/UserGetService.java
index 15fe07f92..1498f0a62 100644
--- a/src/main/java/leets/weeth/domain/user/domain/service/UserGetService.java
+++ b/src/main/java/leets/weeth/domain/user/domain/service/UserGetService.java
@@ -33,6 +33,10 @@ public Optional find(long kakaoId){
return userRepository.findByKakaoId(kakaoId);
}
+ public Optional findByAppleId(String appleId){
+ return userRepository.findByAppleId(appleId);
+ }
+
public List search(String keyword) {
return userRepository.findAllByNameContainingAndStatus(keyword, Status.ACTIVE);
}
diff --git a/src/main/java/leets/weeth/domain/user/presentation/UserController.java b/src/main/java/leets/weeth/domain/user/presentation/UserController.java
index 269293cd0..9c131428a 100644
--- a/src/main/java/leets/weeth/domain/user/presentation/UserController.java
+++ b/src/main/java/leets/weeth/domain/user/presentation/UserController.java
@@ -68,6 +68,20 @@ public CommonResponse integrate(@RequestBody @Valid NormalL
return CommonResponse.createSuccess(SOCIAL_INTEGRATE_SUCCESS.getMessage(), userUseCase.integrate(dto));
}
+ @PostMapping("/apple/login")
+ @Operation(summary = "์ ํ ์์
๋ก๊ทธ์ธ API")
+ public CommonResponse appleLogin(@RequestBody @Valid Login dto) {
+ SocialLoginResponse response = userUseCase.appleLogin(dto);
+ return CommonResponse.createSuccess(SOCIAL_LOGIN_SUCCESS.getMessage(), response);
+ }
+
+ @PostMapping("/apple/register")
+ @Operation(summary = "์ ํ ์์
ํ์๊ฐ์
(dev ์ ์ฉ - ๋ฐ๋ก ACTIVE)")
+ public CommonResponse appleRegister(@RequestBody @Valid Register dto) {
+ userUseCase.appleRegister(dto);
+ return CommonResponse.createSuccess(USER_APPLY_SUCCESS.getMessage());
+ }
+
@GetMapping("/email")
@Operation(summary = "์ด๋ฉ์ผ ์ค๋ณต ํ์ธ")
public CommonResponse checkEmail(@RequestParam String email) {
diff --git a/src/main/java/leets/weeth/global/auth/apple/AppleAuthService.java b/src/main/java/leets/weeth/global/auth/apple/AppleAuthService.java
new file mode 100644
index 000000000..160788da2
--- /dev/null
+++ b/src/main/java/leets/weeth/global/auth/apple/AppleAuthService.java
@@ -0,0 +1,247 @@
+package leets.weeth.global.auth.apple;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import leets.weeth.global.auth.apple.dto.ApplePublicKey;
+import leets.weeth.global.auth.apple.dto.ApplePublicKeys;
+import leets.weeth.global.auth.apple.dto.AppleTokenResponse;
+import leets.weeth.global.auth.apple.dto.AppleUserInfo;
+import leets.weeth.global.auth.apple.exception.AppleAuthenticationException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Service;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestClient;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.RSAPublicKeySpec;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Base64;
+import java.util.Date;
+import java.util.Map;
+
+@Service
+@Slf4j
+public class AppleAuthService {
+
+ @Value("${auth.providers.apple.client_id}")
+ private String appleClientId;
+
+ @Value("${auth.providers.apple.team_id}")
+ private String appleTeamId;
+
+ @Value("${auth.providers.apple.key_id}")
+ private String appleKeyId;
+
+ @Value("${auth.providers.apple.redirect_uri}")
+ private String redirectUri;
+
+ @Value("${auth.providers.apple.token_uri}")
+ private String tokenUri;
+
+ @Value("${auth.providers.apple.keys_uri}")
+ private String keysUri;
+
+ @Value("${auth.providers.apple.private_key_path}")
+ private String privateKeyPath;
+
+ private final RestClient restClient = RestClient.create();
+
+ /**
+ * Authorization code๋ก ์ ํ ํ ํฐ ์์ฒญ
+ * client_secret์ JWT๋ก ์์ฑ (ES256 ์๊ณ ๋ฆฌ์ฆ)
+ */
+ public AppleTokenResponse getAppleToken(String authCode) {
+ String clientSecret = generateClientSecret();
+
+ MultiValueMap body = new LinkedMultiValueMap<>();
+ body.add("grant_type", "authorization_code");
+ body.add("client_id", appleClientId);
+ body.add("client_secret", clientSecret);
+ body.add("code", authCode);
+ body.add("redirect_uri", redirectUri);
+
+ return restClient.post()
+ .uri(tokenUri)
+ .body(body)
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .retrieve()
+ .body(AppleTokenResponse.class);
+ }
+
+ /**
+ * ID Token ๊ฒ์ฆ ๋ฐ ์ฌ์ฉ์ ์ ๋ณด ์ถ์ถ
+ * ์ ํ์ ๋ณ๋ userInfo ์๋ํฌ์ธํธ๊ฐ ์๊ณ ID Token์ ์ ๋ณด๊ฐ ํฌํจ๋จ
+ */
+ public AppleUserInfo verifyAndDecodeIdToken(String idToken) {
+ try {
+ // 1. ID Token์ ํค๋์์ kid ์ถ์ถ
+ String[] tokenParts = idToken.split("\\.");
+ String header = new String(Base64.getUrlDecoder().decode(tokenParts[0]));
+ Map headerMap = parseJson(header);
+ String kid = (String) headerMap.get("kid");
+
+ // 2. ์ ํ ๊ณต๊ฐํค ๊ฐ์ ธ์ค๊ธฐ
+ ApplePublicKeys publicKeys = restClient.get()
+ .uri(keysUri)
+ .retrieve()
+ .body(ApplePublicKeys.class);
+
+ // 3. kid์ ์ผ์นํ๋ ๊ณต๊ฐํค ์ฐพ๊ธฐ
+ ApplePublicKey matchedKey = publicKeys.keys().stream()
+ .filter(key -> key.kid().equals(kid))
+ .findFirst()
+ .orElseThrow(AppleAuthenticationException::new);
+
+ // 4. ๊ณต๊ฐํค๋ก ID Token ๊ฒ์ฆ
+ PublicKey publicKey = generatePublicKey(matchedKey);
+ Claims claims = Jwts.parserBuilder()
+ .setSigningKey(publicKey)
+ .build()
+ .parseClaimsJws(idToken)
+ .getBody();
+
+ // 5. Claims ๊ฒ์ฆ
+ validateClaims(claims);
+
+ // 6. ์ฌ์ฉ์ ์ ๋ณด ์ถ์ถ
+ String appleId = claims.getSubject();
+ String email = claims.get("email", String.class);
+ Boolean emailVerified = claims.get("email_verified", Boolean.class);
+
+ return AppleUserInfo.builder()
+ .appleId(appleId)
+ .email(email)
+ .emailVerified(emailVerified != null ? emailVerified : false)
+ .build();
+
+ } catch (Exception e) {
+ log.error("์ ํ ID Token ๊ฒ์ฆ ์คํจ", e);
+ throw new AppleAuthenticationException();
+ }
+ }
+
+ /**
+ * ์ ํ ๋ก๊ทธ์ธ์ฉ client_secret ์์ฑ
+ * ES256 ์๊ณ ๋ฆฌ์ฆ์ผ๋ก JWT ์์ฑ (p8 ํค ํ์ผ ์ฌ์ฉ)
+ */
+ private String generateClientSecret() {
+ try (InputStream inputStream = getInputStream(privateKeyPath)) {
+ // p8 ํ์ผ์์ Private Key ์ฝ๊ธฐ
+ String privateKeyContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
+
+ // PEM ํ์์ ํค๋/ํธํฐ ์ ๊ฑฐ
+ privateKeyContent = privateKeyContent
+ .replace("-----BEGIN PRIVATE KEY-----", "")
+ .replace("-----END PRIVATE KEY-----", "")
+ .replaceAll("\\s", "");
+
+ // Private Key ๊ฐ์ฒด ์์ฑ
+ byte[] keyBytes = Base64.getDecoder().decode(privateKeyContent);
+ KeyFactory keyFactory = KeyFactory.getInstance("EC");
+ PrivateKey privateKey = keyFactory.generatePrivate(
+ new java.security.spec.PKCS8EncodedKeySpec(keyBytes)
+ );
+
+ // JWT ์์ฑ
+ LocalDateTime now = LocalDateTime.now();
+ Date issuedAt = Date.from(now.atZone(ZoneId.systemDefault()).toInstant());
+ Date expiration = Date.from(now.plusMonths(5).atZone(ZoneId.systemDefault()).toInstant());
+
+ return Jwts.builder()
+ .setHeaderParam("kid", appleKeyId)
+ .setHeaderParam("alg", "ES256")
+ .setIssuer(appleTeamId)
+ .setIssuedAt(issuedAt)
+ .setExpiration(expiration)
+ .setAudience("https://appleid.apple.com")
+ .setSubject(appleClientId)
+ .signWith(privateKey, SignatureAlgorithm.ES256)
+ .compact();
+
+ } catch (Exception e) {
+ log.error("์ ํ Client Secret ์์ฑ ์คํจ", e);
+ throw new AppleAuthenticationException();
+ }
+ }
+
+ /**
+ * ํ์ผ ๊ฒฝ๋ก์์ InputStream ๊ฐ์ ธ์ค๊ธฐ
+ * ์ ๋ ๊ฒฝ๋ก๋ฉด ํ์ผ ์์คํ
์์, ์๋ ๊ฒฝ๋ก๋ฉด classpath์์ ์ฝ์
+ */
+ private InputStream getInputStream(String path) throws IOException {
+ // ์ ๋ ๊ฒฝ๋ก์ธ ๊ฒฝ์ฐ ํ์ผ ์์คํ
์์ ์ฝ๊ธฐ
+ if (path.startsWith("/") || path.matches("^[A-Za-z]:.*")) {
+ return new FileInputStream(path);
+ }
+ // ์๋ ๊ฒฝ๋ก๋ classpath์์ ์ฝ๊ธฐ
+ return new ClassPathResource(path).getInputStream();
+ }
+
+ /**
+ * ์ ํ ๊ณต๊ฐํค๋ก๋ถํฐ PublicKey ๊ฐ์ฒด ์์ฑ
+ */
+ private PublicKey generatePublicKey(ApplePublicKey applePublicKey) {
+ try {
+ byte[] nBytes = Base64.getUrlDecoder().decode(applePublicKey.n());
+ byte[] eBytes = Base64.getUrlDecoder().decode(applePublicKey.e());
+
+ BigInteger n = new BigInteger(1, nBytes);
+ BigInteger e = new BigInteger(1, eBytes);
+
+ RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e);
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+
+ return keyFactory.generatePublic(publicKeySpec);
+ } catch (Exception ex) {
+ log.error("์ ํ ๊ณต๊ฐํค ์์ฑ ์คํจ", ex);
+ throw new AppleAuthenticationException();
+ }
+ }
+
+ /**
+ * ID Token์ Claims ๊ฒ์ฆ
+ */
+ private void validateClaims(Claims claims) {
+ String iss = claims.getIssuer();
+ String aud = claims.getAudience();
+
+ if (!iss.equals("https://appleid.apple.com")) {
+ throw new RuntimeException("์ ํจํ์ง ์์ ๋ฐ๊ธ์(issuer)์
๋๋ค.");
+ }
+
+ if (!aud.equals(appleClientId)) {
+ throw new RuntimeException("์ ํจํ์ง ์์ ์์ ์(audience)์
๋๋ค.");
+ }
+
+ Date expiration = claims.getExpiration();
+ if (expiration.before(new Date())) {
+ throw new RuntimeException("๋ง๋ฃ๋ ID Token์
๋๋ค.");
+ }
+ }
+
+ /**
+ * JSON ๋ฌธ์์ด์ Map์ผ๋ก ํ์ฑ
+ */
+ @SuppressWarnings("unchecked")
+ private Map parseJson(String json) {
+ try {
+ com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
+ return objectMapper.readValue(json, Map.class);
+ } catch (Exception e) {
+ throw new RuntimeException("JSON ํ์ฑ ์คํจ");
+ }
+ }
+}
diff --git a/src/main/java/leets/weeth/global/auth/apple/dto/ApplePublicKey.java b/src/main/java/leets/weeth/global/auth/apple/dto/ApplePublicKey.java
new file mode 100644
index 000000000..54d687297
--- /dev/null
+++ b/src/main/java/leets/weeth/global/auth/apple/dto/ApplePublicKey.java
@@ -0,0 +1,13 @@
+package leets.weeth.global.auth.apple.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public record ApplePublicKey(
+ String kty,
+ String kid,
+ String use,
+ String alg,
+ String n,
+ String e
+) {
+}
diff --git a/src/main/java/leets/weeth/global/auth/apple/dto/ApplePublicKeys.java b/src/main/java/leets/weeth/global/auth/apple/dto/ApplePublicKeys.java
new file mode 100644
index 000000000..909d2fc58
--- /dev/null
+++ b/src/main/java/leets/weeth/global/auth/apple/dto/ApplePublicKeys.java
@@ -0,0 +1,8 @@
+package leets.weeth.global.auth.apple.dto;
+
+import java.util.List;
+
+public record ApplePublicKeys(
+ List keys
+) {
+}
diff --git a/src/main/java/leets/weeth/global/auth/apple/dto/AppleTokenResponse.java b/src/main/java/leets/weeth/global/auth/apple/dto/AppleTokenResponse.java
new file mode 100644
index 000000000..2c52a44a8
--- /dev/null
+++ b/src/main/java/leets/weeth/global/auth/apple/dto/AppleTokenResponse.java
@@ -0,0 +1,10 @@
+package leets.weeth.global.auth.apple.dto;
+
+public record AppleTokenResponse(
+ String access_token,
+ String token_type,
+ Long expires_in,
+ String refresh_token,
+ String id_token
+) {
+}
diff --git a/src/main/java/leets/weeth/global/auth/apple/dto/AppleUserInfo.java b/src/main/java/leets/weeth/global/auth/apple/dto/AppleUserInfo.java
new file mode 100644
index 000000000..8bec95699
--- /dev/null
+++ b/src/main/java/leets/weeth/global/auth/apple/dto/AppleUserInfo.java
@@ -0,0 +1,11 @@
+package leets.weeth.global.auth.apple.dto;
+
+import lombok.Builder;
+
+@Builder
+public record AppleUserInfo(
+ String appleId,
+ String email,
+ Boolean emailVerified
+) {
+}
diff --git a/src/main/java/leets/weeth/global/auth/apple/exception/AppleAuthenticationException.java b/src/main/java/leets/weeth/global/auth/apple/exception/AppleAuthenticationException.java
new file mode 100644
index 000000000..292165235
--- /dev/null
+++ b/src/main/java/leets/weeth/global/auth/apple/exception/AppleAuthenticationException.java
@@ -0,0 +1,9 @@
+package leets.weeth.global.auth.apple.exception;
+
+import leets.weeth.global.common.exception.BusinessLogicException;
+
+public class AppleAuthenticationException extends BusinessLogicException {
+ public AppleAuthenticationException() {
+ super(401, "์ ํ ๋ก๊ทธ์ธ์ ์คํจํ์ต๋๋ค.");
+ }
+}
diff --git a/src/main/java/leets/weeth/global/auth/jwt/filter/JwtAuthenticationProcessingFilter.java b/src/main/java/leets/weeth/global/auth/jwt/filter/JwtAuthenticationProcessingFilter.java
index 4097fca97..1e78d7546 100644
--- a/src/main/java/leets/weeth/global/auth/jwt/filter/JwtAuthenticationProcessingFilter.java
+++ b/src/main/java/leets/weeth/global/auth/jwt/filter/JwtAuthenticationProcessingFilter.java
@@ -47,6 +47,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
if (jwtProvider.validate(accessToken)) {
saveAuthentication(accessToken);
}
+ } catch (TokenNotFoundException e) {
+ log.debug("Token not found: {}", e.getMessage());
} catch (RuntimeException e) {
log.info("error token: {}", e.getMessage());
}
diff --git a/src/main/java/leets/weeth/global/config/SecurityConfig.java b/src/main/java/leets/weeth/global/config/SecurityConfig.java
index d8e245f50..a0233f0d5 100644
--- a/src/main/java/leets/weeth/global/config/SecurityConfig.java
+++ b/src/main/java/leets/weeth/global/config/SecurityConfig.java
@@ -18,6 +18,7 @@
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -73,13 +74,20 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.authorizeHttpRequests(
authorize ->
authorize
- .requestMatchers("/api/v1/users/kakao/login", "api/v1/users/kakao/register", "api/v1/users/kakao/link", "/api/v1/users/apply", "/api/v1/users/email", "/api/v1/users/refresh").permitAll()
+ .requestMatchers("/api/v1/users/kakao/login", "api/v1/users/kakao/register", "api/v1/users/kakao/link", "/api/v1/users/apple/login", "/api/v1/users/apple/register", "/api/v1/users/apply", "/api/v1/users/email", "/api/v1/users/refresh").permitAll()
.requestMatchers("/health-check").permitAll()
- .requestMatchers("/oauth2/**", "/.well-known/**", "/kakao/oauth").permitAll()
+ .requestMatchers("/oauth2/**", "/.well-known/**", "/kakao/oauth", "/apple/oauth").permitAll()
.requestMatchers("/admin", "/admin/login", "/admin/account", "/admin/meeting", "/admin/member", "/admin/penalty",
"/js/**", "/img/**", "/scss/**", "/vendor/**").permitAll()
// ์ค์จ๊ฑฐ ๊ฒฝ๋ก
.requestMatchers("/v3/api-docs", "/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**", "/swagger/**").permitAll()
+ .requestMatchers("/actuator/prometheus")
+ .access((authentication, context) -> {
+ String ip = context.getRequest().getRemoteAddr();
+ boolean allowed = ip.startsWith("172.") || ip.equals("127.0.0.1");
+ return new AuthorizationDecision(allowed);
+ })
+ .requestMatchers("/actuator/health").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
diff --git a/src/main/java/leets/weeth/global/sas/application/exception/AppleLoginException.java b/src/main/java/leets/weeth/global/sas/application/exception/AppleLoginException.java
new file mode 100644
index 000000000..6e3bd0aaa
--- /dev/null
+++ b/src/main/java/leets/weeth/global/sas/application/exception/AppleLoginException.java
@@ -0,0 +1,10 @@
+package leets.weeth.global.sas.application.exception;
+
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+
+public class AppleLoginException extends OAuth2AuthenticationException {
+ public AppleLoginException(String message) {
+ super(new OAuth2Error(ErrorMessage.APPLE_AUTH_ERROR.getCode(), message, null));
+ }
+}
diff --git a/src/main/java/leets/weeth/global/sas/application/exception/ErrorMessage.java b/src/main/java/leets/weeth/global/sas/application/exception/ErrorMessage.java
index 82d24c54d..a897fa5d9 100644
--- a/src/main/java/leets/weeth/global/sas/application/exception/ErrorMessage.java
+++ b/src/main/java/leets/weeth/global/sas/application/exception/ErrorMessage.java
@@ -9,7 +9,8 @@ public enum ErrorMessage {
USER_INACTIVE("WAE-001", "๊ฐ์
์น์ธ์ด ํ๊ฐ๋์ง ์์ ๊ณ์ ์
๋๋ค."),
USER_NOT_FOUND("WAE-002", "์กด์ฌํ์ง ์๋ ์ ์ ์
๋๋ค."),
- KAKAO_AUTH_ERROR("WAE-003", "์นด์นด์ค ๋ก๊ทธ์ธ ์์ธ์
๋๋ค.");
+ KAKAO_AUTH_ERROR("WAE-003", "์นด์นด์ค ๋ก๊ทธ์ธ ์์ธ์
๋๋ค."),
+ APPLE_AUTH_ERROR("WAE-004", "์ ํ ๋ก๊ทธ์ธ ์์ธ์
๋๋ค.");
private final String code;
private final String description;
diff --git a/src/main/java/leets/weeth/global/sas/application/mapper/OAuth2AuthorizationConverter.java b/src/main/java/leets/weeth/global/sas/application/mapper/OAuth2AuthorizationConverter.java
index 169d157eb..1f42d1239 100644
--- a/src/main/java/leets/weeth/global/sas/application/mapper/OAuth2AuthorizationConverter.java
+++ b/src/main/java/leets/weeth/global/sas/application/mapper/OAuth2AuthorizationConverter.java
@@ -1,6 +1,7 @@
package leets.weeth.global.sas.application.mapper;
+import leets.weeth.global.sas.config.grant.AppleGrantType;
import leets.weeth.global.sas.config.grant.KakaoGrantType;
import leets.weeth.global.sas.domain.entity.*;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
@@ -33,6 +34,10 @@ public static OAuth2AuthorizationGrantAuthorization convertOAuth2AuthorizationGr
return convertKakaoAuthorizationGrantAuthorization(authorization);
}
+ if (AppleGrantType.APPLE_IDENTITY_TOKEN.equals(grantType)) {
+ return convertAppleAuthorizationGrantAuthorization(authorization);
+ }
+
return null;
}
@@ -55,6 +60,25 @@ public static OAuth2AuthorizationGrantAuthorization convertOAuth2AuthorizationGr
.build();
}
+ private static OidcAuthorizationCodeGrantAuthorization
+ convertAppleAuthorizationGrantAuthorization(OAuth2Authorization authorization) {
+
+ AccessToken accessToken = extractAccessToken(authorization);
+ RefreshToken refreshToken = extractRefreshToken(authorization);
+ IdToken idToken = extractIdToken(authorization);
+
+ return OidcAuthorizationCodeGrantAuthorization.builder()
+ .id(authorization.getId())
+ .registeredClientId(authorization.getRegisteredClientId())
+ .principalName(authorization.getPrincipalName())
+ .authorizedScopes(authorization.getAuthorizedScopes())
+ .accessToken(accessToken)
+ .refreshToken(refreshToken)
+ .idToken(idToken)
+ .principal(authorization.getAttribute(Principal.class.getName()))
+ .build();
+ }
+
static OidcAuthorizationCodeGrantAuthorization convertOidcAuthorizationCodeGrantAuthorization(OAuth2Authorization authorization) {
AuthorizationCode authorizationCode = extractAuthorizationCode(authorization);
AccessToken accessToken = extractAccessToken(authorization);
diff --git a/src/main/java/leets/weeth/global/sas/application/usecase/AuthUsecase.java b/src/main/java/leets/weeth/global/sas/application/usecase/AuthUsecase.java
index 22de81b81..a110df157 100644
--- a/src/main/java/leets/weeth/global/sas/application/usecase/AuthUsecase.java
+++ b/src/main/java/leets/weeth/global/sas/application/usecase/AuthUsecase.java
@@ -6,6 +6,9 @@
import leets.weeth.domain.user.domain.entity.User;
import leets.weeth.domain.user.domain.service.UserCardinalGetService;
import leets.weeth.domain.user.domain.service.UserGetService;
+import leets.weeth.global.auth.apple.AppleAuthService;
+import leets.weeth.global.auth.apple.dto.AppleTokenResponse;
+import leets.weeth.global.auth.apple.dto.AppleUserInfo;
import leets.weeth.global.auth.jwt.service.JwtService;
import leets.weeth.global.auth.kakao.KakaoAuthService;
import leets.weeth.global.auth.kakao.dto.KakaoTokenResponse;
@@ -22,6 +25,7 @@
public class AuthUsecase {
private final KakaoAuthService kakaoAuthService;
+ private final AppleAuthService appleAuthService;
private final UserGetService userGetService;
private final JwtService jwtService;
private final UserCardinalGetService userCardinalGetService;
@@ -51,6 +55,32 @@ public User login(String authCode) {
return user;
}
+ /*
+ ํ์ ์์
+ */
+ public User appleLogin(String authCode, String idToken) {
+ AppleTokenResponse tokenResponse = appleAuthService.getAppleToken(authCode);
+
+ // ID Token ์ฌ์ฉ
+ String token = idToken != null ? idToken : tokenResponse.id_token();
+ AppleUserInfo userInfo = appleAuthService.verifyAndDecodeIdToken(token);
+
+ String appleId = userInfo.appleId();
+ Optional optionalUser = userGetService.findByAppleId(appleId);
+
+ if (optionalUser.isEmpty()) {
+ throw new UserNotFoundException(); // -> Weeth ํ์๊ฐ์
ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ
+ }
+
+ User user = optionalUser.get();
+
+ if (user.isInactive()) {
+ throw new UserInActiveException(); // -> ๊ฐ์
์น์ธ ๋๊ธฐ
+ }
+
+ return user;
+ }
+
public OauthUserInfoResponse userInfo(String accessToken) {
String token = accessToken.substring(7);
diff --git a/src/main/java/leets/weeth/global/sas/config/OAuth2AuthorizationServerConfig.java b/src/main/java/leets/weeth/global/sas/config/OAuth2AuthorizationServerConfig.java
index 86183202b..a1d39fa6f 100644
--- a/src/main/java/leets/weeth/global/sas/config/OAuth2AuthorizationServerConfig.java
+++ b/src/main/java/leets/weeth/global/sas/config/OAuth2AuthorizationServerConfig.java
@@ -5,13 +5,12 @@
import com.nimbusds.jose.proc.SecurityContext;
import leets.weeth.domain.user.domain.entity.SecurityUser;
import leets.weeth.domain.user.domain.service.UserGetService;
+import leets.weeth.global.auth.apple.AppleAuthService;
import leets.weeth.global.auth.kakao.KakaoAuthService;
import leets.weeth.global.sas.application.exception.Oauth2JwtTokenException;
import leets.weeth.global.sas.application.property.OauthProperties;
import leets.weeth.global.sas.config.authentication.ProviderAwareEntryPoint;
-import leets.weeth.global.sas.config.grant.KakaoAccessTokenAuthenticationConverter;
-import leets.weeth.global.sas.config.grant.KakaoAuthenticationProvider;
-import leets.weeth.global.sas.config.grant.KakaoGrantType;
+import leets.weeth.global.sas.config.grant.*;
import leets.weeth.global.sas.domain.repository.OAuth2AuthorizationGrantAuthorizationRepository;
import leets.weeth.global.sas.domain.service.RedisOAuth2AuthorizationService;
import lombok.RequiredArgsConstructor;
@@ -31,7 +30,6 @@
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
-import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
@@ -56,6 +54,7 @@ public class OAuth2AuthorizationServerConfig {
private final ProviderAwareEntryPoint entryPoint;
private final KakaoAuthService kakaoAuthService;
+ private final AppleAuthService appleAuthService;
private final UserGetService userGetService;
private final OauthProperties props;
@@ -68,25 +67,57 @@ public class OAuth2AuthorizationServerConfig {
@Order(1) // ์ฐ์ ์์๋ฅผ ๊ธฐ๋ณธ filter๋ณด๋ค ๋๊ฒ ์ค์
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
KakaoAccessTokenAuthenticationConverter kakaoConverter,
- KakaoAuthenticationProvider kakaoProvider) throws Exception {
- OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
+ KakaoAuthenticationProvider kakaoProvider,
+ AppleIdentityTokenAuthenticationConverter appleConverter,
+ AppleAuthenticationProvider appleProvider) throws Exception { // entryPoint ์ฃผ์
- http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
- .oidc(Customizer.withDefaults())
- .tokenEndpoint(token -> token
- .accessTokenRequestConverters(c -> c.add(kakaoConverter))
- .authenticationProviders(p -> p.add(kakaoProvider))
- );
+ // 1. Configurer ์ธ์คํด์ค ์์ฑ (๊ณต์ ํ
ํ๋ฆฟ ๋ฐฉ์)
+ OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
+ OAuth2AuthorizationServerConfigurer.authorizationServer();
- http.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
+ http
+ // 2. ์ด ํํฐ์ฒด์ธ์ด ์ ์ฉ๋ ์๋ํฌ์ธํธ๋ฅผ ๋ช
์์ ์ผ๋ก ์ง์ (ํ
ํ๋ฆฟ ๋ฐฉ์)
+ .securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
- // ์ปค์คํ
EntryPoint (provider ํ๋ผ๋ฏธํฐ ํด์ โ 302 /oauth2/authorization/{provider})
- http.exceptionHandling(e -> e.defaultAuthenticationEntryPointFor(
- entryPoint, rq -> rq.getRequestURI().startsWith("/oauth2/authorize")));
+ // 3. .with()๋ฅผ ์ฌ์ฉํ์ฌ Configurer ์ ์ฉ ๋ฐ ์ปค์คํ
(ํ
ํ๋ฆฟ ๋ฐฉ์)
+ .with(authorizationServerConfigurer, (authorizationServer) ->
+ authorizationServer
+ .oidc(Customizer.withDefaults()) // OIDC ํ์ฑํ
- return http
- .csrf(csrf -> csrf.ignoringRequestMatchers("/oauth2/**", "/.well-known/**"))
- .build();
+ // 4. [์ฌ์ฉ์ ์ ์] ํ ํฐ ์๋ํฌ์ธํธ ์ปค์คํ
๋ก์ง ์ฝ์
+ .tokenEndpoint(token -> token
+ .accessTokenRequestConverters(c -> {
+ c.add(kakaoConverter);
+ c.add(appleConverter);
+ })
+ .authenticationProviders(p -> {
+ p.add(kakaoProvider);
+ p.add(appleProvider);
+ })
+ )
+ )
+
+ // 5. ์๋ํฌ์ธํธ์ ๋ํ ๊ธฐ๋ณธ ์ธ์ฆ ์๊ตฌ (ํ
ํ๋ฆฟ ๋ฐฉ์)
+ .authorizeHttpRequests((authorize) ->
+ authorize.anyRequest().authenticated()
+ )
+
+ // 6. [์ฌ์ฉ์ ์ ์] ๋ฆฌ์์ค ์๋ฒ ์ค์ (JWT ๊ฒ์ฆ)
+ .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
+
+ // 7. [์ฌ์ฉ์ ์ ์] ์ธ์ฆ ์คํจ ์ ์ปค์คํ
EntryPoint ์ฌ์ฉ (ํ
ํ๋ฆฟ ๊ตฌ์กฐ + ์ฌ์ฉ์ ๋ก์ง)
+ // (ํ
ํ๋ฆฟ์ /login ๋ฆฌ๋๋ ์
๋์ , ๊ธฐ์กด์ provider ๋ถ๊ธฐ ๋ก์ง์ ์ฌ์ฉ)
+ .exceptionHandling((exceptions) -> exceptions
+ .defaultAuthenticationEntryPointFor(
+ entryPoint, // ์ฌ์ฉ์์ ์ปค์คํ
EntryPoint
+ rq -> rq.getRequestURI().startsWith("/oauth2/authorize") // ์ฌ์ฉ์์ ์ปค์คํ
Predicate
+ )
+ )
+
+ // 8. [์ฌ์ฉ์ ์ ์] CSRF ์ค์
+ .csrf(csrf -> csrf.ignoringRequestMatchers("/oauth2/**", "/.well-known/**"));
+
+ return http.build();
}
@Bean
@@ -104,6 +135,7 @@ public RegisteredClientRepository registeredClientRepository() {
type.add(AuthorizationGrantType.AUTHORIZATION_CODE);
type.add(AuthorizationGrantType.REFRESH_TOKEN);
type.add(KakaoGrantType.KAKAO_ACCESS_TOKEN);
+ type.add(AppleGrantType.APPLE_IDENTITY_TOKEN);
})
.redirectUris(uri -> {
uri.addAll(leenk.getRedirectUris());
@@ -203,6 +235,20 @@ KakaoAuthenticationProvider kakaoProvider(
kakaoAuthService, userGetService, authorizationService, tokenGenerator);
}
+ @Bean
+ AppleIdentityTokenAuthenticationConverter appleConverter() {
+ return new AppleIdentityTokenAuthenticationConverter();
+ }
+
+ @Bean
+ AppleAuthenticationProvider appleProvider(
+ OAuth2AuthorizationService authorizationService,
+ OAuth2TokenGenerator> tokenGenerator) {
+
+ return new AppleAuthenticationProvider(
+ appleAuthService, userGetService, authorizationService, tokenGenerator);
+ }
+
private RSAKey loadRsaKeyFromString() {
try {
return new RSAKey.Builder(publicKey)
diff --git a/src/main/java/leets/weeth/global/sas/config/grant/AppleAuthenticationProvider.java b/src/main/java/leets/weeth/global/sas/config/grant/AppleAuthenticationProvider.java
new file mode 100644
index 000000000..d18db726c
--- /dev/null
+++ b/src/main/java/leets/weeth/global/sas/config/grant/AppleAuthenticationProvider.java
@@ -0,0 +1,73 @@
+package leets.weeth.global.sas.config.grant;
+
+import leets.weeth.domain.user.domain.entity.User;
+import leets.weeth.domain.user.domain.service.UserGetService;
+import leets.weeth.global.auth.apple.AppleAuthService;
+import leets.weeth.global.auth.apple.dto.AppleUserInfo;
+import leets.weeth.global.sas.application.exception.AppleLoginException;
+import leets.weeth.global.sas.application.exception.UserInActiveException;
+import leets.weeth.global.sas.application.exception.UserNotFoundException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+import org.springframework.stereotype.Component;
+
+@Component
+public class AppleAuthenticationProvider extends CustomAuthenticationProvider {
+
+ private final AppleAuthService appleAuthService;
+ private final UserGetService userGetService;
+
+ public AppleAuthenticationProvider(
+ AppleAuthService appleAuthService,
+ UserGetService userGetService,
+ OAuth2AuthorizationService authorizationService,
+ OAuth2TokenGenerator extends OAuth2Token> tokenGenerator
+ ) {
+ super(authorizationService, tokenGenerator);
+ this.appleAuthService = appleAuthService;
+ this.userGetService = userGetService;
+ }
+
+ @Override
+ protected AuthorizationGrantType getGrantTokenType() {
+ return AppleGrantType.APPLE_IDENTITY_TOKEN;
+ }
+
+ @Override
+ protected Class extends Authentication> getAuthenticationClass() {
+ return AppleIdentityTokenAuthenticationToken.class;
+ }
+
+ @Override
+ protected String extractAccessToken(Authentication authentication) {
+ AppleIdentityTokenAuthenticationToken grantAuth =
+ (AppleIdentityTokenAuthenticationToken) authentication;
+ return grantAuth.getAppleIdentityToken();
+ }
+
+ @Override
+ protected AppleUserInfo getUserInfo(String identityToken) {
+ try {
+ // Identity Token ๊ฒ์ฆ ๋ฐ ์ฌ์ฉ์ ์ ๋ณด ์ถ์ถ
+ return appleAuthService.verifyAndDecodeIdToken(identityToken);
+ } catch (Exception e) {
+ throw new AppleLoginException(e.getMessage());
+ }
+ }
+
+ @Override
+ protected User getOrLoadUser(AppleUserInfo userInfo) {
+ String appleId = userInfo.appleId();
+ User user = userGetService.findByAppleId(appleId)
+ .orElseThrow(UserNotFoundException::new);
+
+ if (user.isInactive()) {
+ throw new UserInActiveException();
+ }
+
+ return user;
+ }
+}
diff --git a/src/main/java/leets/weeth/global/sas/config/grant/AppleGrantType.java b/src/main/java/leets/weeth/global/sas/config/grant/AppleGrantType.java
new file mode 100644
index 000000000..78155dfa0
--- /dev/null
+++ b/src/main/java/leets/weeth/global/sas/config/grant/AppleGrantType.java
@@ -0,0 +1,10 @@
+package leets.weeth.global.sas.config.grant;
+
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+
+public final class AppleGrantType {
+ public static final AuthorizationGrantType APPLE_IDENTITY_TOKEN =
+ new AuthorizationGrantType("apple_identity_token");
+
+ private AppleGrantType() {}
+}
diff --git a/src/main/java/leets/weeth/global/sas/config/grant/AppleIdentityTokenAuthenticationConverter.java b/src/main/java/leets/weeth/global/sas/config/grant/AppleIdentityTokenAuthenticationConverter.java
new file mode 100644
index 000000000..dd9c5cdcc
--- /dev/null
+++ b/src/main/java/leets/weeth/global/sas/config/grant/AppleIdentityTokenAuthenticationConverter.java
@@ -0,0 +1,37 @@
+package leets.weeth.global.sas.config.grant;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.util.StringUtils;
+
+import java.util.HashMap;
+
+public class AppleIdentityTokenAuthenticationConverter implements AuthenticationConverter {
+
+ @Override
+ public Authentication convert(HttpServletRequest request) {
+ if (!AppleGrantType.APPLE_IDENTITY_TOKEN.getValue()
+ .equals(request.getParameter(OAuth2ParameterNames.GRANT_TYPE))) {
+ return null;
+ }
+
+ String identityToken = request.getParameter("identity_token");
+ if (!StringUtils.hasText(identityToken)) {
+ throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
+ }
+
+ Authentication clientPrincipal = (Authentication) request.getUserPrincipal();
+
+ var additional = new HashMap();
+ request.getParameterMap().forEach((k, v) -> {
+ if (!OAuth2ParameterNames.GRANT_TYPE.equals(k) && !"identity_token".equals(k))
+ additional.put(k, v[0]);
+ });
+
+ return new AppleIdentityTokenAuthenticationToken(identityToken, clientPrincipal, additional);
+ }
+}
diff --git a/src/main/java/leets/weeth/global/sas/config/grant/AppleIdentityTokenAuthenticationToken.java b/src/main/java/leets/weeth/global/sas/config/grant/AppleIdentityTokenAuthenticationToken.java
new file mode 100644
index 000000000..02947407f
--- /dev/null
+++ b/src/main/java/leets/weeth/global/sas/config/grant/AppleIdentityTokenAuthenticationToken.java
@@ -0,0 +1,22 @@
+package leets.weeth.global.sas.config.grant;
+
+import lombok.Getter;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
+
+import java.util.Map;
+
+@Getter
+public class AppleIdentityTokenAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
+
+ private final String appleIdentityToken;
+
+ public AppleIdentityTokenAuthenticationToken(
+ String appleIdentityToken,
+ Authentication clientPrincipal,
+ Map additionalParameters) {
+ super(new AuthorizationGrantType("apple_identity_token"), clientPrincipal, additionalParameters);
+ this.appleIdentityToken = appleIdentityToken;
+ }
+}
diff --git a/src/main/java/leets/weeth/global/sas/domain/service/RedisOAuth2AuthorizationService.java b/src/main/java/leets/weeth/global/sas/domain/service/RedisOAuth2AuthorizationService.java
index 4909fc5aa..a361ed0b9 100644
--- a/src/main/java/leets/weeth/global/sas/domain/service/RedisOAuth2AuthorizationService.java
+++ b/src/main/java/leets/weeth/global/sas/domain/service/RedisOAuth2AuthorizationService.java
@@ -36,9 +36,15 @@ public void save(OAuth2Authorization authorization) {
// ttl ์ค์
if (entity.getAccessToken() == null && entity instanceof OAuth2AuthorizationCodeGrantAuthorization codeGrant) {
- entity.updateExpire(calculateTtlSeconds(((OAuth2AuthorizationCodeGrantAuthorization) entity).getAuthorizationCode().getExpiresAt()));
- } else {
+ entity.updateExpire(calculateTtlSeconds(codeGrant.getAuthorizationCode().getExpiresAt()));
+ } else if (entity.getRefreshToken() != null) {
entity.updateExpire(calculateTtlSeconds(entity.getRefreshToken().getExpiresAt()));
+ } else if (entity.getAccessToken() != null) {
+ // refresh token์ด ์์ผ๋ฉด access token์ ๋ง๋ฃ ์๊ฐ ์ฌ์ฉ
+ entity.updateExpire(calculateTtlSeconds(entity.getAccessToken().getExpiresAt()));
+ } else {
+ // access token๋ ์์ผ๋ฉด ๊ธฐ๋ณธ๊ฐ์ผ๋ก 1์๊ฐ ์ค์
+ entity.updateExpire(3600L);
}
this.authorizationGrantAuthorizationRepository.save(entity);
diff --git a/src/main/java/leets/weeth/global/sas/presentation/AuthController.java b/src/main/java/leets/weeth/global/sas/presentation/AuthController.java
index 6329292f1..8627b09f1 100644
--- a/src/main/java/leets/weeth/global/sas/presentation/AuthController.java
+++ b/src/main/java/leets/weeth/global/sas/presentation/AuthController.java
@@ -7,7 +7,6 @@
import leets.weeth.global.sas.application.dto.OauthUserInfoResponse;
import leets.weeth.global.sas.application.usecase.AuthUsecase;
import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
@@ -47,6 +46,24 @@ public void kakaoCallback(@RequestParam String code,
savedRequestHandler.onAuthenticationSuccess(request, response, auth);
}
+ @GetMapping("/apple/oauth")
+ public void appleCallback(@RequestParam String code,
+ @RequestParam(required = false) String id_token,
+ HttpServletRequest request,
+ HttpServletResponse response) throws Exception {
+
+ User findUser = authUsecase.appleLogin(code, id_token);
+
+ Authentication auth = new UsernamePasswordAuthenticationToken(SecurityUser.from(findUser), null, List.of(new SimpleGrantedAuthority(findUser.getRole().name())));
+
+ SecurityContext context = SecurityContextHolder.createEmptyContext();
+ context.setAuthentication(auth);
+ SecurityContextHolder.setContext(context);
+ securityContextRepository.saveContext(context, request, response);
+
+ savedRequestHandler.onAuthenticationSuccess(request, response, auth);
+ }
+
@GetMapping("/user/me")
public OauthUserInfoResponse userInfo(@RequestHeader("Authorization") String accessToken) {
return authUsecase.userInfo(accessToken);
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 3de76ac2e..94b3fd9b0 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -27,6 +27,14 @@ auth:
grant_type: ${KAKAO_GRANT_TYPE}
token_uri: ${KAKAO_TOKEN_URI}
user_info_uri: ${KAKAO_USER_INFO_URI}
+ apple:
+ client_id: ${APPLE_CLIENT_ID}
+ team_id: ${APPLE_TEAM_ID}
+ key_id: ${APPLE_KEY_ID}
+ redirect_uri: ${APPLE_REDIRECT_URI}
+ token_uri: https://appleid.apple.com/auth/token
+ keys_uri: https://appleid.apple.com/auth/keys
+ private_key_path: ${APPLE_PRIVATE_KEY_PATH}
jwt:
private-key: ${JWT_PRIVATE_KEY}
public-key: ${JWT_PUBLIC_KEY}
@@ -39,3 +47,15 @@ auth:
scopes: ${LEENK_SCOPES}
accessTokenTtl: ${LEENK_ACCESS_TOKEN_TTL}
refreshTokenTtl: ${LEENK_REFRESH_TOKEN_TTL}
+
+management:
+ endpoints:
+ web:
+ exposure:
+ include:
+ - health
+ - prometheus
+ prometheus:
+ metrics:
+ export:
+ enabled: true
diff --git a/src/test/java/leets/weeth/config/TestContainersConfig.java b/src/test/java/leets/weeth/config/TestContainersConfig.java
new file mode 100644
index 000000000..273f39d42
--- /dev/null
+++ b/src/test/java/leets/weeth/config/TestContainersConfig.java
@@ -0,0 +1,20 @@
+package leets.weeth.config;
+
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.context.annotation.Bean;
+import org.testcontainers.mysql.MySQLContainer;
+import org.testcontainers.utility.DockerImageName;
+
+@TestConfiguration
+public class TestContainersConfig {
+
+ private static final String MYSQL_IMAGE = "mysql:8.0.41";
+
+ @Bean
+ @ServiceConnection
+ public MySQLContainer mysqlContainer() {
+ return new MySQLContainer(DockerImageName.parse(MYSQL_IMAGE))
+ .withReuse(true);
+ }
+}
diff --git a/src/test/java/leets/weeth/config/TestContainersTest.java b/src/test/java/leets/weeth/config/TestContainersTest.java
new file mode 100644
index 000000000..80846e01e
--- /dev/null
+++ b/src/test/java/leets/weeth/config/TestContainersTest.java
@@ -0,0 +1,25 @@
+package leets.weeth.config;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.*;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.context.annotation.Import;
+import org.testcontainers.mysql.MySQLContainer;
+
+@DataJpaTest
+@Import(TestContainersConfig.class)
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+class TestContainersTest {
+
+ @Autowired
+ private MySQLContainer mysqlContainer;
+
+ @Test
+ void ์ค์ ํ์ผ๋ก_์ฃผ์
๋_์ปจํ
์ด๋_์ ์_๋์_ํ
์คํธ() {
+ assertThat(mysqlContainer).isNotNull();
+ assertThat(mysqlContainer.isRunning()).isTrue();
+ System.out.println("Container JDBC URL: " + mysqlContainer.getJdbcUrl());
+ }
+}
diff --git a/src/test/java/leets/weeth/domain/attendance/application/mapper/AttendanceMapperTest.java b/src/test/java/leets/weeth/domain/attendance/application/mapper/AttendanceMapperTest.java
new file mode 100644
index 000000000..20578feee
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/attendance/application/mapper/AttendanceMapperTest.java
@@ -0,0 +1,124 @@
+package leets.weeth.domain.attendance.application.mapper;
+
+import static leets.weeth.domain.attendance.test.fixture.AttendanceTestFixture.*;
+import static org.assertj.core.api.Assertions.*;
+
+import java.time.LocalDate;
+import java.util.List;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import leets.weeth.domain.attendance.application.dto.AttendanceDTO;
+import leets.weeth.domain.attendance.domain.entity.Attendance;
+import leets.weeth.domain.schedule.domain.entity.Meeting;
+import leets.weeth.domain.user.domain.entity.User;
+import leets.weeth.domain.user.domain.entity.enums.Position;
+
+class AttendanceMapperTest {
+
+ private final AttendanceMapper attendanceMapper = new AttendanceMapperImpl();
+
+ @Test
+ @DisplayName("toMainDto: ์ฌ์ฉ์ + ๋น์ผ ์ถ์ ๊ฐ์ฒด๋ฅผ Main DTO๋ก ๋งคํํ๋ค")
+ void toMainDto_mapsUserAndTodayAttendance() {
+ // given
+ LocalDate today = LocalDate.now();
+ Meeting meeting = createOneDayMeeting(today, 1, 1111, "Today");
+ User user = createActiveUserWithAttendances("์ด์งํ", List.of(meeting));
+ Attendance attendance = user.getAttendances().get(0);
+
+ // when
+ AttendanceDTO.Main main = attendanceMapper.toMainDto(user, attendance);
+
+ // then
+ assertThat(main).isNotNull();
+ assertThat(main.title()).isEqualTo("Today");
+ assertThat(main.status()).isEqualTo(attendance.getStatus());
+ assertThat(main.start()).isEqualTo(meeting.getStart());
+ assertThat(main.end()).isEqualTo(meeting.getEnd());
+ assertThat(main.location()).isEqualTo(meeting.getLocation());
+ }
+
+ @Test
+ @DisplayName("toResponseDto: ๋จ์ผ ์ถ์์ Response DTO๋ก ๋งคํํ๋ค")
+ void toResponseDto_mapsSingleAttendance() {
+ // given
+ Meeting meeting = createOneDayMeeting(LocalDate.now().minusDays(1), 1, 2222, "D-1");
+ User user = createActiveUser("์ฌ์ฉ์A");
+ Attendance attendance = createAttendance(meeting, user);
+
+ // when
+ AttendanceDTO.Response response = attendanceMapper.toResponseDto(attendance);
+
+ // then
+ assertThat(response).isNotNull();
+ assertThat(response.title()).isEqualTo("D-1");
+ assertThat(response.start()).isEqualTo(meeting.getStart());
+ assertThat(response.end()).isEqualTo(meeting.getEnd());
+ assertThat(response.location()).isEqualTo(meeting.getLocation());
+ }
+
+ @Test
+ @DisplayName("toDetailDto: ์ฌ์ฉ์ + Response ๋ฆฌ์คํธ๋ฅผ Detail DTO๋ก ๋งคํ(total = attend + absence)")
+ void toDetailDto_mapsDetailAndTotal() {
+ // given
+ LocalDate base = LocalDate.now();
+ Meeting m1 = createOneDayMeeting(base.minusDays(2), 1, 1000, "D-2");
+ Meeting m2 = createOneDayMeeting(base.minusDays(1), 1, 1001, "D-1");
+ User user = createActiveUser("์ด์งํ");
+ setUserAttendanceStats(user, 3, 2);
+
+ Attendance a1 = createAttendance(m1, user);
+ Attendance a2 = createAttendance(m2, user);
+
+ AttendanceDTO.Response r1 = attendanceMapper.toResponseDto(a1);
+ AttendanceDTO.Response r2 = attendanceMapper.toResponseDto(a2);
+
+ // when
+ AttendanceDTO.Detail detail = attendanceMapper.toDetailDto(user, List.of(r1, r2));
+
+ // then
+ assertThat(detail).isNotNull();
+ assertThat(detail.attendances()).hasSize(2);
+ assertThat(detail.total()).isEqualTo(5);
+ }
+
+ @Test
+ @DisplayName("toAttendanceInfoDto: Attendance๋ฅผ Info DTO๋ก ๋งคํ")
+ void toAttendanceInfoDto_mapsInfo() {
+ // given
+ Meeting meeting = createOneDayMeeting(LocalDate.now(), 1, 3333, "Info");
+ User user = createActiveUser("์ ์ B");
+ enrichUserProfile(user, Position.BE, "์ปดํจํฐ๊ณตํ๊ณผ", "20201234");
+
+ Attendance attendance = createAttendance(meeting, user);
+ setAttendanceId(attendance, 10L);
+
+ // when
+ AttendanceDTO.AttendanceInfo info = attendanceMapper.toAttendanceInfoDto(attendance);
+
+ // then
+ assertThat(info).isNotNull();
+ assertThat(info.id()).isEqualTo(10L);
+ assertThat(info.status()).isEqualTo(attendance.getStatus());
+ assertThat(info.name()).isEqualTo("์ ์ B");
+ }
+
+ @Test
+ @DisplayName("null ์์ ์ฑ ํ
์คํธ: todayAttendance๊ฐ null์ด๋ฉด ํ๋๋ null๋ก ๋งคํ")
+ void nullSafety_whenTodayAttendanceNull() {
+ // given
+ User user = createActiveUser("์ด์งํ");
+
+ // when
+ AttendanceDTO.Main main = attendanceMapper.toMainDto(user, null);
+
+ // then
+ assertThat(main).isNotNull();
+ assertThat(main.title()).isNull();
+ assertThat(main.start()).isNull();
+ assertThat(main.end()).isNull();
+ assertThat(main.location()).isNull();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/leets/weeth/domain/attendance/application/usecase/AttendanceUseCaseImplTest.java b/src/test/java/leets/weeth/domain/attendance/application/usecase/AttendanceUseCaseImplTest.java
new file mode 100644
index 000000000..eb3957ca5
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/attendance/application/usecase/AttendanceUseCaseImplTest.java
@@ -0,0 +1,304 @@
+package leets.weeth.domain.attendance.application.usecase;
+
+import leets.weeth.domain.attendance.application.dto.AttendanceDTO;
+import leets.weeth.domain.attendance.application.exception.AttendanceCodeMismatchException;
+import leets.weeth.domain.attendance.application.exception.AttendanceNotFoundException;
+import leets.weeth.domain.attendance.application.mapper.AttendanceMapper;
+import leets.weeth.domain.attendance.domain.entity.Attendance;
+import leets.weeth.domain.attendance.domain.entity.enums.Status;
+import leets.weeth.domain.attendance.domain.service.AttendanceGetService;
+import leets.weeth.domain.attendance.domain.service.AttendanceUpdateService;
+import leets.weeth.domain.schedule.application.exception.MeetingNotFoundException;
+import leets.weeth.domain.schedule.domain.entity.Meeting;
+import leets.weeth.domain.schedule.domain.service.MeetingGetService;
+import leets.weeth.domain.user.domain.entity.Cardinal;
+import leets.weeth.domain.user.domain.entity.User;
+import leets.weeth.domain.user.domain.service.UserCardinalGetService;
+import leets.weeth.domain.user.domain.service.UserGetService;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static leets.weeth.domain.attendance.test.fixture.AttendanceTestFixture.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.BDDMockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class AttendanceUseCaseImplTest {
+
+ private final Long userId = 10L;
+ @Mock private UserGetService userGetService;
+ @Mock private UserCardinalGetService userCardinalGetService;
+ @Mock private AttendanceGetService attendanceGetService;
+ @Mock private AttendanceUpdateService attendanceUpdateService;
+ @Mock private AttendanceMapper attendanceMapper;
+ @Mock private MeetingGetService meetingGetService;
+ @InjectMocks private AttendanceUseCaseImpl attendanceUseCase;
+
+ @Test
+ @DisplayName("find: ์ฌ๋ฌ ๋ ์ง์ ์ถ์ ๋ชฉ๋ก ์ค '์์/์ข
๋ฃ ๋ ์ง๊ฐ ๋ชจ๋ ์ค๋'์ธ ์ถ์์ ๋ณด๋ฅผ ์ ํ")
+ void find_todayMeeting_filtersDispersedAttendances() {
+ // given
+ LocalDate today = LocalDate.now();
+
+ Meeting meetingYesterday = createOneDayMeeting(today.minusDays(1), 1, 1111, "Yesterday");
+ Meeting meetingToday = createOneDayMeeting(today, 1, 2222, "Today");
+ Meeting meetingTomorrow = createOneDayMeeting(today.plusDays(1),1, 3333, "Tomorrow");
+
+ User user = createActiveUserWithAttendances(
+ "์ด์งํ", List.of(meetingYesterday, meetingToday, meetingTomorrow)
+ );
+
+ Attendance expectedTodayAttendance = user.getAttendances().stream()
+ .filter(attendance -> "Today".equals(attendance.getMeeting().getTitle()))
+ .findFirst()
+ .orElseThrow();
+
+ AttendanceDTO.Main mapped = mock(AttendanceDTO.Main.class);
+
+ given(userGetService.find(userId)).willReturn(user);
+ given(attendanceMapper.toMainDto(eq(user), eq(expectedTodayAttendance))).willReturn(mapped);
+
+ // when
+ AttendanceDTO.Main actual = attendanceUseCase.find(userId);
+
+ // then
+ assertThat(actual).isSameAs(mapped);
+ then(attendanceMapper).should().toMainDto(eq(user), eq(expectedTodayAttendance));
+ then(attendanceMapper).shouldHaveNoMoreInteractions();
+ }
+
+ @Test
+ @DisplayName("10๋ถ ์ ๋ถํฐ ์ถ์์ด ๊ฐ๋ฅํ์ง ํ์ธ")
+ void checkIn_10MinutesBeforeMeeting_ShouldSucceed() {
+ // given
+ LocalDateTime now = LocalDateTime.now();
+ Meeting meeting = Meeting.builder()
+ .start(now.plusMinutes(5)) // 5๋ถ ๋ค ์์ (checkIn ๋ก์ง์ '10๋ถ ์ ' ๋ฒ์ ๋ด)
+ .end(now.plusHours(2))
+ .code(1234)
+ .title("Today")
+ .cardinal(1)
+ .build();
+
+ User user = createActiveUserWithAttendances(
+ "์ด์งํ", List.of(meeting)
+ );
+
+ when(userGetService.find(userId)).thenReturn(user);
+
+
+ // when & then
+ assertDoesNotThrow(() -> attendanceUseCase.checkIn(userId, 1234));
+ verify(attendanceUpdateService, times(1)).attend(any(Attendance.class));
+ }
+
+ @Test
+ @DisplayName("11๋ถ ์ ์ ์ถ์์ ์ค๋ฅ ํ์ธ")
+ void checkIn_10MinutesBeforeMeeting_ShouldFailed() {
+ // given
+ LocalDateTime now = LocalDateTime.now();
+ Meeting meeting = Meeting.builder()
+ .start(now.plusMinutes(11)) // 11๋ถ๋ค ์์ -> ์ค๋ฅ ๋ฐ์ํด์ผํจ
+ .end(now.plusHours(2))
+ .code(1234)
+ .title("Today")
+ .cardinal(1)
+ .build();
+
+ User user = createActiveUserWithAttendances(
+ "์ด์งํ", List.of(meeting)
+ );
+
+ when(userGetService.find(userId)).thenReturn(user);
+
+
+ // when & then
+ assertThatThrownBy(() -> attendanceUseCase.checkIn(userId, 1234))
+ .isInstanceOf(AttendanceNotFoundException.class);
+ }
+ @Test
+ @DisplayName("find: '์์/์ข
๋ฃ ๋ ์ง๊ฐ ๋ชจ๋ ์ค๋'์ธ ์ถ์์ด ์๋ค๋ฉด, mapper.toMainDto(user, null)์ ํธ์ถ")
+ void find_noExactToday_returnsNullMapped() {
+ // given
+ LocalDate today = LocalDate.now();
+
+ Meeting yesterdayMeeting = createOneDayMeeting(today.minusDays(1), 1, 1111, "Yesterday");
+ Meeting tomorrowMeeting = createOneDayMeeting(today.plusDays(1), 1, 3333, "Tomorrow");
+
+ User user = createActiveUserWithAttendances("์ด์งํ",
+ List.of(yesterdayMeeting, tomorrowMeeting));
+
+ when(userGetService.find(userId)).thenReturn(user);
+ AttendanceDTO.Main mapped = mock(AttendanceDTO.Main.class);
+ when(attendanceMapper.toMainDto(user, null)).thenReturn(mapped);
+
+ // when
+ AttendanceDTO.Main actual = attendanceUseCase.find(userId);
+
+ // then
+ assertThat(actual).isSameAs(mapped);
+ verify(attendanceMapper).toMainDto(user, null);
+ }
+
+ @Test
+ @DisplayName("findAllDetailsByCurrentCardinal: ํ์ฌ ๊ธฐ์๋ง ํํฐ๋งยท์ ๋ ฌํ์ฌ Detail ๋งคํ")
+ void findAllDetailsByCurrentCardinal() {
+ // given
+ LocalDate today = LocalDate.now();
+ Meeting meetingDayMinus1 = createOneDayMeeting(today.minusDays(1), 1, 1111, "D-1");
+ Meeting meetingToday = createOneDayMeeting(today, 1, 2222, "D-Day");
+ User user = createActiveUserWithAttendances("์ด์งํ", List.of(meetingDayMinus1, meetingToday));
+
+ List userAttendances = user.getAttendances();
+ Attendance attendanceFirst = userAttendances.get(0); // D-1
+ Attendance attendanceSecond = userAttendances.get(1); // D-Day
+
+ when(userGetService.find(userId)).thenReturn(user);
+ Cardinal currentCardinal = mock(Cardinal.class);
+ when(currentCardinal.getCardinalNumber()).thenReturn(1);
+ when(userCardinalGetService.getCurrentCardinal(user)).thenReturn(currentCardinal);
+
+ AttendanceDTO.Response responseFirst = mock(AttendanceDTO.Response.class);
+ AttendanceDTO.Response responseSecond = mock(AttendanceDTO.Response.class);
+ when(attendanceMapper.toResponseDto(attendanceFirst)).thenReturn(responseFirst);
+ when(attendanceMapper.toResponseDto(attendanceSecond)).thenReturn(responseSecond);
+
+ AttendanceDTO.Detail expectedDetail = mock(AttendanceDTO.Detail.class);
+ when(attendanceMapper.toDetailDto(eq(user), anyList())).thenReturn(expectedDetail);
+
+ // when
+ AttendanceDTO.Detail actualDetail = attendanceUseCase.findAllDetailsByCurrentCardinal(userId);
+
+ // then
+ assertThat(actualDetail).isSameAs(expectedDetail);
+ verify(attendanceMapper).toDetailDto(eq(user), argThat(list -> list.size() == 2));
+ }
+
+ @Test
+ @DisplayName("close(now, cardinal): ๋น์ผ ์ ๊ธฐ๋ชจ์์ ์ฐพ์ close")
+ void close_success() {
+ // given
+ LocalDate now = LocalDate.now();
+ Meeting targetMeeting = createOneDayMeeting(now, 1, 1111, "Today");
+ Meeting otherMeeting = createOneDayMeeting(now.minusDays(1), 1, 9999, "Yesterday");
+
+ Attendance attendance1 = mock(Attendance.class);
+ Attendance attendance2 = mock(Attendance.class);
+
+ when(meetingGetService.find(1)).thenReturn(List.of(targetMeeting, otherMeeting));
+ when(attendanceGetService.findAllByMeeting(targetMeeting)).thenReturn(List.of(attendance1, attendance2));
+
+ // when
+ attendanceUseCase.close(now, 1);
+
+ // then
+ verify(attendanceUpdateService).close(argThat(list ->
+ list.size() == 2 && list.containsAll(List.of(attendance1, attendance2))
+ ));
+ }
+
+ @Test
+ @DisplayName("close(now, cardinal): ๋น์ผ ์ ๊ธฐ๋ชจ์์ด ์์ผ๋ฉด MeetingNotFoundException")
+ void close_notFound() {
+ // given
+ LocalDate now = LocalDate.now();
+ Meeting otherDayMeeting = createOneDayMeeting(now.minusDays(1), 1, 9999, "Yesterday");
+
+ when(meetingGetService.find(1)).thenReturn(List.of(otherDayMeeting));
+
+ // when & then
+ assertThatThrownBy(() -> attendanceUseCase.close(now, 1))
+ .isInstanceOf(MeetingNotFoundException.class);
+ }
+
+ @Nested
+ @DisplayName("checkIn")
+ class CheckInTest {
+
+ @Test
+ @DisplayName("์งํ ์ค ์ ๊ธฐ๋ชจ์์ด๊ณ ์ฝ๋ ์ผ์นํ๋ฉฐ ์ํ๊ฐ ATTEND๊ฐ ์๋๋ฉด ์ถ์ ์ฒ๋ฆฌ")
+ void checkIn_success() {
+ // given
+ User user = mock(User.class);
+ Meeting inProgressMeeting = createInProgressMeeting(1, 1234, "InProgress");
+ Attendance attendance = mock(Attendance.class);
+ when(attendance.getMeeting()).thenReturn(inProgressMeeting);
+ when(attendance.isWrong(1234)).thenReturn(false);
+ when(attendance.getStatus()).thenReturn(Status.PENDING);
+
+ when(userGetService.find(userId)).thenReturn(user);
+ when(user.getAttendances()).thenReturn(List.of(attendance));
+
+ // when
+ attendanceUseCase.checkIn(userId, 1234);
+
+ // then
+ verify(attendanceUpdateService).attend(attendance);
+ }
+
+ @Test
+ @DisplayName("์งํ ์ค ์ ๊ธฐ๋ชจ์์ด ์์ผ๋ฉด AttendanceNotFoundException")
+ void checkIn_notFoundMeeting() {
+ // given
+ User user = mock(User.class);
+ when(userGetService.find(userId)).thenReturn(user);
+ when(user.getAttendances()).thenReturn(List.of());
+
+ // when & then
+ assertThatThrownBy(() -> attendanceUseCase.checkIn(userId, 1234))
+ .isInstanceOf(AttendanceNotFoundException.class);
+ }
+
+ @Test
+ @DisplayName("์ฝ๋ ๋ถ์ผ์น ์ AttendanceCodeMismatchException")
+ void checkIn_wrongCode() {
+ // given
+ User user = mock(User.class);
+ Meeting inProgressMeeting = createInProgressMeeting(1, 1234, "InProgress");
+
+ Attendance attendance = mock(Attendance.class);
+ when(attendance.getMeeting()).thenReturn(inProgressMeeting);
+ when(attendance.isWrong(9999)).thenReturn(true);
+
+ when(userGetService.find(userId)).thenReturn(user);
+ when(user.getAttendances()).thenReturn(List.of(attendance));
+
+ // when & then
+ assertThatThrownBy(() -> attendanceUseCase.checkIn(userId, 9999))
+ .isInstanceOf(AttendanceCodeMismatchException.class);
+ }
+
+ @Test
+ @DisplayName("์ด๋ฏธ ATTEND๋ฉด ์ถ๊ฐ ์ฒ๋ฆฌ ์์ด ์ข
๋ฃ")
+ void checkIn_alreadyAttend() {
+ // given
+ User user = mock(User.class);
+ Meeting inProgressMeeting = createInProgressMeeting(1, 1234, "InProgress");
+
+ Attendance attendance = mock(Attendance.class);
+ when(attendance.getMeeting()).thenReturn(inProgressMeeting);
+ when(attendance.isWrong(1234)).thenReturn(false);
+ when(attendance.getStatus()).thenReturn(Status.ATTEND);
+
+ when(userGetService.find(userId)).thenReturn(user);
+ when(user.getAttendances()).thenReturn(List.of(attendance));
+
+ // when
+ attendanceUseCase.checkIn(userId, 1234);
+
+ // then
+ verify(attendanceUpdateService, never()).attend(any());
+ }
+ }
+}
diff --git a/src/test/java/leets/weeth/domain/attendance/domain/repository/AttendanceRepositoryTest.java b/src/test/java/leets/weeth/domain/attendance/domain/repository/AttendanceRepositoryTest.java
new file mode 100644
index 000000000..e41ab9462
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/attendance/domain/repository/AttendanceRepositoryTest.java
@@ -0,0 +1,85 @@
+package leets.weeth.domain.attendance.domain.repository;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.context.annotation.Import;
+
+import leets.weeth.config.TestContainersConfig;
+import leets.weeth.domain.attendance.domain.entity.Attendance;
+import leets.weeth.domain.schedule.domain.entity.Meeting;
+import leets.weeth.domain.schedule.domain.entity.enums.MeetingStatus;
+import leets.weeth.domain.schedule.domain.repository.MeetingRepository;
+import leets.weeth.domain.user.domain.entity.User;
+import leets.weeth.domain.user.domain.entity.enums.Status;
+import leets.weeth.domain.user.domain.repository.UserRepository;
+
+@DataJpaTest
+@Import(TestContainersConfig.class)
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+public class AttendanceRepositoryTest {
+
+ @Autowired private AttendanceRepository attendanceRepository;
+ @Autowired private MeetingRepository meetingRepository;
+ @Autowired private UserRepository userRepository;
+
+ private Meeting meeting;
+ private User activeUser1;
+ private User activeUser2;
+
+ @BeforeEach
+ void setUp() {
+ meeting = Meeting.builder()
+ .title("1์ฐจ ์ ๊ธฐ๋ชจ์")
+ .start(LocalDateTime.now().minusHours(1))
+ .end(LocalDateTime.now().plusHours(1))
+ .code(1234)
+ .cardinal(1)
+ .meetingStatus(MeetingStatus.OPEN)
+ .build();
+ meetingRepository.save(meeting);
+
+ activeUser1 = User.builder().name("์ด์งํ").status(Status.ACTIVE).build();
+ activeUser2 = User.builder().name("์ด๊ฐํ").status(Status.ACTIVE).build();
+ userRepository.saveAll(List.of(activeUser1, activeUser2));
+
+ attendanceRepository.save(new Attendance(meeting, activeUser1));
+ attendanceRepository.save(new Attendance(meeting, activeUser2));
+ }
+
+ @Test
+ @DisplayName("ํน์ ์ ๊ธฐ๋ชจ์ + ์ฌ์ฉ์ ์ํ๋ก ์ถ์ ๋ชฉ๋ก ์กฐํ")
+ void findAllByMeetingAndUserStatus() {
+ // given
+ Status active = activeUser1.getStatus();
+
+ // when
+ List attendances =
+ attendanceRepository.findAllByMeetingAndUserStatus(meeting, active);
+
+ // then
+ assertThat(attendances).hasSize(2);
+ assertThat(attendances)
+ .extracting(a -> a.getUser().getName())
+ .containsExactlyInAnyOrder("์ด์งํ", "์ด๊ฐํ");
+ }
+
+ @Test
+ @DisplayName("ํน์ ์ ๊ธฐ๋ชจ์์ ๋ชจ๋ ์ถ์ ๋ ์ฝ๋ ์ญ์ ")
+ void deleteAllByMeeting() {
+ // when
+ attendanceRepository.deleteAllByMeeting(meeting);
+
+ // then
+ List after = attendanceRepository.findAll();
+ assertThat(after).isEmpty();
+ }
+}
diff --git a/src/test/java/leets/weeth/domain/attendance/domain/service/AttendanceSaveServiceTest.java b/src/test/java/leets/weeth/domain/attendance/domain/service/AttendanceSaveServiceTest.java
new file mode 100644
index 000000000..b8b8c45e8
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/attendance/domain/service/AttendanceSaveServiceTest.java
@@ -0,0 +1,73 @@
+package leets.weeth.domain.attendance.domain.service;
+
+import static leets.weeth.domain.attendance.test.fixture.AttendanceTestFixture.*;
+import static leets.weeth.domain.schedule.test.fixture.ScheduleTestFixture.*;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import leets.weeth.domain.attendance.domain.entity.Attendance;
+import leets.weeth.domain.attendance.domain.repository.AttendanceRepository;
+import leets.weeth.domain.schedule.domain.entity.Meeting;
+import leets.weeth.domain.user.domain.entity.User;
+
+@ExtendWith(MockitoExtension.class)
+class AttendanceSaveServiceTest {
+
+ @Mock private AttendanceRepository attendanceRepository;
+ @InjectMocks private AttendanceSaveService attendanceSaveService;
+
+ @Test
+ @DisplayName("init(user, meetings): ๊ฐ ์ ๊ธฐ๋ชจ์์ ๋ํ Attendance ์ ์ฅ ํ user.add ํธ์ถ")
+ void init_createsAttendanceAndLinkToUser() {
+ // given
+ User user = mock(User.class);
+ Meeting meetingFirst = createMeeting();
+ Meeting meetingSecond = createMeeting();
+
+ when(attendanceRepository.save(any(Attendance.class)))
+ .thenAnswer(invocation -> invocation.getArgument(0));
+
+ // when
+ attendanceSaveService.init(user, List.of(meetingFirst, meetingSecond));
+
+ // then
+ verify(attendanceRepository, times(2)).save(any(Attendance.class));
+ verify(user, times(2)).add(any(Attendance.class));
+ }
+
+ @Test
+ @DisplayName("saveAll(users, meeting): ์ฌ์ฉ์ ์๋งํผ Attendance ์์ฑ ํ saveAll ํธ์ถ")
+ void saveAll_bulkInsert() {
+ // given
+ Meeting meeting = createMeeting();
+ User userFirst = createActiveUser("์ด์งํ");
+ User userSecond = createActiveUser("์ด๊ฐํ");
+
+ // when
+ attendanceSaveService.saveAll(List.of(userFirst, userSecond), meeting);
+
+ // then
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor> listCaptor = ArgumentCaptor.forClass(List.class);
+ verify(attendanceRepository).saveAll(listCaptor.capture());
+
+ List savedAttendances = listCaptor.getValue();
+ Assertions.assertThat(savedAttendances).hasSize(2);
+
+ Assertions.assertThat(savedAttendances)
+ .allSatisfy(att -> Assertions.assertThat(att.getMeeting()).isSameAs(meeting));
+ Assertions.assertThat(savedAttendances)
+ .extracting(Attendance::getUser)
+ .containsExactlyInAnyOrder(userFirst, userSecond);
+ }
+}
diff --git a/src/test/java/leets/weeth/domain/attendance/domain/service/AttendanceUpdateServiceTest.java b/src/test/java/leets/weeth/domain/attendance/domain/service/AttendanceUpdateServiceTest.java
new file mode 100644
index 000000000..2682ac48a
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/attendance/domain/service/AttendanceUpdateServiceTest.java
@@ -0,0 +1,107 @@
+package leets.weeth.domain.attendance.domain.service;
+
+import static leets.weeth.domain.attendance.test.fixture.AttendanceTestFixture.*;
+import static leets.weeth.domain.schedule.test.fixture.ScheduleTestFixture.*;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import leets.weeth.domain.attendance.domain.entity.Attendance;
+import leets.weeth.domain.attendance.domain.entity.enums.Status;
+import leets.weeth.domain.schedule.domain.entity.Meeting;
+import leets.weeth.domain.user.domain.entity.User;
+
+class AttendanceUpdateServiceTest {
+
+ private final AttendanceUpdateService attendanceUpdateService = new AttendanceUpdateService();
+
+ @Test
+ @DisplayName("attend(): attendance.attend() + user.attend() ํธ์ถ")
+ void attend_callsEntityMethods() {
+ // given
+ Meeting meeting = createMeeting();
+ User realUser = createActiveUser("์ด์งํ");
+ User userSpy = spy(realUser);
+
+ doNothing().when(userSpy).attend();
+
+ Attendance realAttendance = createAttendance(meeting, userSpy);
+ Attendance attendanceSpy = spy(realAttendance);
+
+ // when
+ attendanceUpdateService.attend(attendanceSpy);
+
+ // then
+ verify(attendanceSpy).attend();
+ verify(userSpy).attend();
+ }
+
+ @Test
+ @DisplayName("close(): pending๋ง close() + user.absent() ํธ์ถ")
+ void close_onlyPendingIsClosed() {
+ // given
+ Meeting meeting = createMeeting();
+
+ User pendingUserReal = createActiveUser("pending-user");
+ User nonPendingUserReal = createActiveUser("non-pending-user");
+ User pendingUserSpy = spy(pendingUserReal);
+ User nonPendingUserSpy = spy(nonPendingUserReal);
+
+ doNothing().when(pendingUserSpy).absent();
+ doNothing().when(nonPendingUserSpy).absent();
+
+ Attendance pendingAttendanceReal = createAttendance(meeting, pendingUserSpy);
+ Attendance nonPendingAttendanceReal = createAttendance(meeting, nonPendingUserSpy);
+ Attendance pendingAttendanceSpy = spy(pendingAttendanceReal);
+ Attendance nonPendingAttendanceSpy = spy(nonPendingAttendanceReal);
+
+ doReturn(true).when(pendingAttendanceSpy).isPending();
+ doReturn(false).when(nonPendingAttendanceSpy).isPending();
+
+ // when
+ attendanceUpdateService.close(List.of(pendingAttendanceSpy, nonPendingAttendanceSpy));
+
+ // then
+ verify(pendingAttendanceSpy).close();
+ verify(pendingUserSpy).absent();
+
+ verify(nonPendingAttendanceSpy, never()).close();
+ verify(nonPendingUserSpy, never()).absent();
+ }
+
+ @Test
+ @DisplayName("updateUserAttendanceByStatus: ATTEND๋ฉด user.removeAttend(), ๊ทธ ์ธ์๋ user.removeAbsent()")
+ void updateUserAttendanceByStatus() {
+ // given
+ Meeting meeting = createMeeting();
+
+ User attendUserReal = createActiveUser("attend-user");
+ User absentUserReal = createActiveUser("absent-user");
+
+ User attendUserSpy = spy(attendUserReal);
+ User absentUserSpy = spy(absentUserReal);
+
+ doNothing().when(attendUserSpy).removeAttend();
+ doNothing().when(absentUserSpy).removeAbsent();
+
+ Attendance attendAttendanceReal = createAttendance(meeting, attendUserSpy);
+ Attendance absentAttendanceReal = createAttendance(meeting, absentUserSpy);
+ Attendance attendAttendanceSpy = spy(attendAttendanceReal);
+ Attendance absentAttendanceSpy = spy(absentAttendanceReal);
+
+ doReturn(Status.ATTEND).when(attendAttendanceSpy).getStatus();
+ doReturn(Status.ABSENT).when(absentAttendanceSpy).getStatus();
+ doReturn(attendUserSpy).when(attendAttendanceSpy).getUser();
+ doReturn(absentUserSpy).when(absentAttendanceSpy).getUser();
+
+ // when
+ attendanceUpdateService.updateUserAttendanceByStatus(List.of(attendAttendanceSpy, absentAttendanceSpy));
+
+ // then
+ verify(attendUserSpy).removeAttend();
+ verify(absentUserSpy).removeAbsent();
+ }
+}
diff --git a/src/test/java/leets/weeth/domain/attendance/test/fixture/AttendanceTestFixture.java b/src/test/java/leets/weeth/domain/attendance/test/fixture/AttendanceTestFixture.java
new file mode 100644
index 000000000..6cfe52b8b
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/attendance/test/fixture/AttendanceTestFixture.java
@@ -0,0 +1,101 @@
+package leets.weeth.domain.attendance.test.fixture;
+
+import java.lang.reflect.Field;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+
+import leets.weeth.domain.attendance.domain.entity.Attendance;
+import leets.weeth.domain.schedule.domain.entity.Meeting;
+import leets.weeth.domain.user.domain.entity.User;
+import leets.weeth.domain.user.domain.entity.enums.Department;
+import leets.weeth.domain.user.domain.entity.enums.Position;
+import leets.weeth.domain.user.domain.entity.enums.Status;
+
+public class AttendanceTestFixture {
+
+ private AttendanceTestFixture() {}
+
+ //todo : ์ถํ User Fixture ํ์ฉ ์์
+ public static User createActiveUser(String name) {
+ return User.builder().name(name).status(Status.ACTIVE).build();
+ }
+
+ //todo : ์ถํ User Fixture ํ์ฉ ์์
+ public static User createActiveUserWithAttendances(String name, List meetings) {
+ User user = createActiveUser(name);
+
+ if (user.getAttendances() == null) {
+ try {
+ java.lang.reflect.Field f = user.getClass().getDeclaredField("attendances");
+ f.setAccessible(true);
+ f.set(user, new java.util.ArrayList<>());
+ } catch (Exception ignore) {}
+ }
+ if (meetings != null) {
+ for (Meeting meeting : meetings) {
+ Attendance attendance = createAttendance(meeting, user);
+ user.add(attendance);
+ }
+ }
+ return user;
+ }
+
+ public static Attendance createAttendance(Meeting meeting, User user) {
+ return new Attendance(meeting, user);
+ }
+
+ public static Meeting createOneDayMeeting(LocalDate date, int cardinal, int code, String title) {
+ return Meeting.builder()
+ .title(title)
+ .location("Test Location")
+ .start(date.atTime(10, 0))
+ .end(date.atTime(12, 0))
+ .code(code)
+ .cardinal(cardinal)
+ .build();
+ }
+
+ public static Meeting createInProgressMeeting(int cardinal, int code, String title) {
+ return Meeting.builder()
+ .title(title)
+ .location("Test Location")
+ .start(LocalDateTime.now().minusMinutes(5))
+ .end(LocalDateTime.now().plusMinutes(5))
+ .code(code)
+ .cardinal(cardinal)
+ .build();
+ }
+
+ private static void setField(Object target, String fieldName, Object value) {
+ try {
+ Field field = target.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(target, value);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void setAttendanceId(Attendance attendance, Long id) {
+ setField(attendance, "id", id);
+ }
+
+ public static void setUserAttendanceStats(User user, Integer attendanceCount, Integer absenceCount) {
+ setField(user, "attendanceCount", attendanceCount);
+ setField(user, "absenceCount", absenceCount);
+ }
+
+ public static void enrichUserProfile(User user, Position position, Department department, String studentId) {
+ setField(user, "position", position);
+ setField(user, "department", department);
+ setField(user, "studentId", studentId);
+ }
+
+ public static void enrichUserProfile(User user, Position position, String departmentKoreanValue, String studentId) {
+ setField(user, "position", position);
+ Department department = Department.to(departmentKoreanValue); // โ ํต์ฌ
+ setField(user, "department", department);
+ setField(user, "studentId", studentId);
+ }
+}
diff --git a/src/test/java/leets/weeth/domain/board/application/mapper/PostMapperTest.java b/src/test/java/leets/weeth/domain/board/application/mapper/PostMapperTest.java
new file mode 100644
index 000000000..0f2f006c7
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/board/application/mapper/PostMapperTest.java
@@ -0,0 +1,55 @@
+package leets.weeth.domain.board.application.mapper;
+
+import leets.weeth.domain.board.application.dto.PostDTO;
+import leets.weeth.domain.board.domain.entity.Post;
+import leets.weeth.domain.user.domain.entity.User;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mapstruct.factory.Mappers;
+import org.mockito.InjectMocks;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith(MockitoExtension.class)
+class PostMapperTest {
+
+ @InjectMocks
+ private PostMapper mapper = Mappers.getMapper(PostMapper.class);
+
+ private User testUser;
+ private Post testPost;
+
+ @BeforeEach
+ void setUp() {
+ testUser = User.builder()
+ .id(1L)
+ .name("ํ
์คํธ์ ์ ")
+ .email("test@weeth.com")
+ .build();
+
+ testPost = Post.builder()
+ .id(1L)
+ .title("ํ
์คํธ ๊ฒ์๊ธ")
+ .user(testUser)
+ .content("ํ
์คํธ ๋ด์ฉ์
๋๋ค.")
+ .build();
+ }
+
+ @Test
+ @DisplayName("Post๋ฅผ PostDTO.SaveResponse๋ก ๋ณํ")
+ void toSaveResponse() {
+ // given
+ // testPost ์ฌ์ฉ
+
+ // when
+ PostDTO.SaveResponse response = mapper.toSaveResponse(testPost);
+
+ // then
+ assertThat(response).isNotNull();
+ assertThat(response.id()).isEqualTo(testPost.getId());
+ }
+
+}
diff --git a/src/test/java/leets/weeth/domain/board/application/usecase/NoticeUsecaseImplTest.java b/src/test/java/leets/weeth/domain/board/application/usecase/NoticeUsecaseImplTest.java
new file mode 100644
index 000000000..f03100a94
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/board/application/usecase/NoticeUsecaseImplTest.java
@@ -0,0 +1,194 @@
+package leets.weeth.domain.board.application.usecase;
+
+import leets.weeth.domain.board.application.dto.NoticeDTO;
+import leets.weeth.domain.board.application.mapper.NoticeMapper;
+import leets.weeth.domain.board.domain.entity.Notice;
+import leets.weeth.domain.board.domain.test.fixture.NoticeFixture;
+import leets.weeth.domain.board.domain.service.NoticeFindService;
+import leets.weeth.domain.file.domain.entity.File;
+import leets.weeth.domain.file.domain.service.FileGetService;
+import leets.weeth.domain.user.domain.entity.User;
+import leets.weeth.domain.user.domain.entity.enums.Department;
+import leets.weeth.domain.user.domain.entity.enums.Position;
+import leets.weeth.domain.user.domain.entity.enums.Role;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.*;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.mockito.BDDMockito.*;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+@ExtendWith(MockitoExtension.class)
+class NoticeUsecaseImplTest {
+
+ @Mock
+ private NoticeFindService noticeFindService;
+
+ @Mock
+ private FileGetService fileGetService;
+
+ @InjectMocks
+ private NoticeUsecaseImpl noticeUsecase;
+
+ @Mock
+ private NoticeMapper noticeMapper;
+
+ @Test
+ void ๊ณต์ง์ฌํญ์ด_์ต์ ์์ผ๋ก_์ ๋ ฌ๋๋์ง() {
+ // given
+ User user = User.builder()
+ .email("abc@test.com")
+ .name("ํ๊ธธ๋")
+ .position(Position.BE)
+ .department(Department.SW)
+ .role(Role.USER)
+ .build();
+
+ List notices = new ArrayList<>();
+ for(int i = 0; i<5; i++){
+ Notice notice = NoticeFixture.createNotice("๊ณต์ง" + i, user);
+ ReflectionTestUtils.setField(notice, "id", (long) i + 1);
+ notices.add(notice);
+ }
+
+ Pageable pageable = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "id"));
+
+ Slice slice = new SliceImpl<>(List.of(notices.get(4), notices.get(3), notices.get(2)), pageable, true);
+
+ given(noticeFindService.findRecentNotices(any(Pageable.class))).willReturn(slice);
+ given(fileGetService.findAllByNotice(any())).willReturn(List.of());
+
+ given(noticeMapper.toAll(any(Notice.class), anyBoolean()))
+ .willAnswer(invocation -> {
+ Notice notice = invocation.getArgument(0);
+ return new NoticeDTO.ResponseAll(
+ notice.getId(),
+ notice.getUser() != null ? notice.getUser().getName() : "",
+ notice.getUser() != null ? notice.getUser().getPosition() : Position.BE,
+ notice.getUser() != null ? notice.getUser().getRole() : Role.USER,
+ notice.getTitle(),
+ notice.getContent(),
+ notice.getCreatedAt(),
+ notice.getCommentCount(),
+ false
+ );
+ });
+
+ // when
+ Slice noticeResponses = noticeUsecase.findNotices(0, 3);
+
+ // then
+ assertThat(noticeResponses).isNotNull();
+ assertThat(noticeResponses.getContent()).hasSize(3);
+ assertThat(noticeResponses.getContent())
+ .extracting(NoticeDTO.ResponseAll::title)
+ .containsExactly(
+ notices.get(4).getTitle(),
+ notices.get(3).getTitle(),
+ notices.get(2).getTitle()
+ );
+ assertThat(noticeResponses.hasNext()).isTrue();
+
+ verify(noticeFindService, times(1)).findRecentNotices(pageable);
+
+ }
+
+ @Test
+ void ๊ณต์ง์ฌํญ_๊ฒ์์_๊ฒฐ๊ณผ์_ํ์ผ_์กด์ฌ์ฌ๋ถ๊ฐ_์ ์์ ์ผ๋ก_๋ฐํ() {
+ // given
+ User user = User.builder()
+ .email("abc@test.com")
+ .name("ํ๊ธธ๋")
+ .position(Position.BE)
+ .department(Department.SW)
+ .role(Role.USER)
+ .build();
+
+ List notices = new ArrayList<>();
+ for(int i = 0; i<3; i++){
+ Notice notice = NoticeFixture.createNotice("๊ณต์ง" + i, user);
+ ReflectionTestUtils.setField(notice, "id", (long) i + 1);
+ notices.add(notice);
+ }
+ for(int i = 3; i<6; i++){
+ Notice notice = NoticeFixture.createNotice("๊ฒ์" + i, user);
+ ReflectionTestUtils.setField(notice, "id", (long) i + 1);
+ notices.add(notice);
+ }
+
+
+ Pageable pageable = PageRequest.of(0, 5, Sort.by(Sort.Direction.DESC, "id"));
+
+ Slice slice = new SliceImpl<>(List.of(notices.get(5), notices.get(4), notices.get(3)), pageable, false);
+
+ given(noticeFindService.search(any(String.class), any(Pageable.class))).willReturn(slice);
+ // ์ง์ id - ํ์ผ ์กด์ฌ, ํ์ id - ํ์ผ ์์ (๋น ๋ฆฌ์คํธ)
+ given(fileGetService.findAllByNotice(any()))
+ .willAnswer(invocation -> {
+ Long noticeId = invocation.getArgument(0);
+ if (noticeId % 2 == 0) {
+ return List.of(File.builder()
+ .notice(notices.get((int)(noticeId-1)))
+ .build());
+ } else {
+ return List.of();
+ }
+ });
+
+ given(noticeMapper.toAll(any(Notice.class), anyBoolean()))
+ .willAnswer(invocation -> {
+ Notice notice = invocation.getArgument(0);
+ boolean fileExists = invocation.getArgument(1);
+ return new NoticeDTO.ResponseAll(
+ notice.getId(),
+ notice.getUser() != null ? notice.getUser().getName() : "",
+ notice.getUser() != null ? notice.getUser().getPosition() : Position.BE,
+ notice.getUser() != null ? notice.getUser().getRole() : Role.USER,
+ notice.getTitle(),
+ notice.getContent(),
+ notice.getCreatedAt(),
+ notice.getCommentCount(),
+ fileExists
+ );
+ });
+
+ // when
+ Slice noticeResponses = noticeUsecase.searchNotice("๊ฒ์", 0, 5);
+
+ // then
+ assertThat(noticeResponses).isNotNull();
+ assertThat(noticeResponses.getContent()).hasSize(3);
+ assertThat(noticeResponses.getContent())
+ .extracting(NoticeDTO.ResponseAll::title)
+ .containsExactly(
+ notices.get(5).getTitle(),
+ notices.get(4).getTitle(),
+ notices.get(3).getTitle()
+ );
+ assertThat(noticeResponses.hasNext()).isFalse();
+
+ // ์ง์ id : ํ์ผ ์กด์ฌ, ํ์ id : ํ์ผ ์์ ๊ฒ์ฆ
+ assertThat(noticeResponses.getContent().get(0).hasFile()).isTrue();
+ assertThat(noticeResponses.getContent().get(1).hasFile()).isFalse();
+
+ verify(noticeFindService, times(1)).search("๊ฒ์", pageable);
+ }
+
+ @Disabled("TODO: update ๊ธฐ๋ฅ ํ
์คํธ ๊ตฌํ ํ์")
+ @Test
+ void update() {
+ }
+
+ @Disabled("TODO: delete ๊ธฐ๋ฅ ํ
์คํธ ๊ตฌํ ํ์")
+ @Test
+ void delete() {
+ }
+}
diff --git a/src/test/java/leets/weeth/domain/board/domain/repository/NoticeRepositoryTest.java b/src/test/java/leets/weeth/domain/board/domain/repository/NoticeRepositoryTest.java
new file mode 100644
index 000000000..2c017a487
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/board/domain/repository/NoticeRepositoryTest.java
@@ -0,0 +1,87 @@
+package leets.weeth.domain.board.domain.repository;
+
+import leets.weeth.config.TestContainersConfig;
+import leets.weeth.domain.board.domain.entity.Notice;
+import leets.weeth.domain.board.domain.test.fixture.NoticeFixture;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.Sort;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+@DataJpaTest
+@Import(TestContainersConfig.class)
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+class NoticeRepositoryTest {
+
+ @Autowired
+ private NoticeRepository noticeRepository;
+
+ @Test
+ void findPageBy_๊ณต์ง_id_๋ด๋ฆผ์ฐจ์์ผ๋ก_์กฐํ() {
+ // given
+ List notices = new ArrayList<>();
+ for(int i = 0; i<5; i++){
+ Notice notice = NoticeFixture.createNotice("๊ณต์ง" + i);
+ notices.add(notice);
+ }
+
+ noticeRepository.saveAll(notices);
+ Pageable pageable = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "id"));
+
+ // when
+ Slice pagedNotices = noticeRepository.findPageBy(pageable);
+
+ // then
+ assertThat(pagedNotices.getSize()).isEqualTo(3);
+ assertThat(pagedNotices)
+ .extracting(Notice::getTitle)
+ .containsExactly(
+ notices.get(4).getTitle(),
+ notices.get(3).getTitle(),
+ notices.get(2).getTitle()
+ );
+ assertThat(pagedNotices.hasNext()).isTrue();
+ }
+
+ @Test
+ void search_๊ฒ์์ด๊ฐ_ํฌํจ๋_๊ณต์ง_id_๋ด๋ฆผ์ฐจ์์ผ๋ก_์กฐํ() {
+ // given
+ List notices = new ArrayList<>();
+ for(int i = 0; i<6; i++){
+ Notice notice;
+ if(i % 2 == 0){
+ notice = NoticeFixture.createNotice("๊ณต์ง" + i);
+ } else{
+ notice = NoticeFixture.createNotice("๊ฒ์" + i);
+ }
+ notices.add(notice);
+ }
+
+ noticeRepository.saveAll(notices);
+
+ Pageable pageable = PageRequest.of(0, 5, Sort.by(Sort.Direction.DESC, "id"));
+
+ // when
+ Slice searchedNotices = noticeRepository.search("๊ฒ์", pageable);
+
+ // then
+ assertThat(searchedNotices.getContent()).hasSize(3);
+ assertThat(searchedNotices.getContent())
+ .extracting(Notice::getTitle)
+ .containsExactly(notices.get(5).getTitle(),
+ notices.get(3).getTitle(),
+ notices.get(1).getTitle());
+ assertThat(searchedNotices.hasNext()).isFalse();
+
+ }
+}
diff --git a/src/test/java/leets/weeth/domain/board/domain/test/fixture/NoticeFixture.java b/src/test/java/leets/weeth/domain/board/domain/test/fixture/NoticeFixture.java
new file mode 100644
index 000000000..5bd9e00e4
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/board/domain/test/fixture/NoticeFixture.java
@@ -0,0 +1,23 @@
+package leets.weeth.domain.board.domain.test.fixture;
+
+import leets.weeth.domain.board.domain.entity.Notice;
+import leets.weeth.domain.user.domain.entity.User;
+
+public class NoticeFixture {
+ public static Notice createNotice(String title, User user){
+ return Notice.builder()
+ .title(title)
+ .content("๋ด์ฉ")
+ .user(user)
+ .commentCount(0)
+ .build();
+ }
+
+ public static Notice createNotice(String title){
+ return Notice.builder()
+ .title(title)
+ .content("๋ด์ฉ")
+ .commentCount(0)
+ .build();
+ }
+}
diff --git a/src/test/java/leets/weeth/domain/schedule/test/fixture/ScheduleTestFixture.java b/src/test/java/leets/weeth/domain/schedule/test/fixture/ScheduleTestFixture.java
new file mode 100644
index 000000000..92fcf7b3a
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/schedule/test/fixture/ScheduleTestFixture.java
@@ -0,0 +1,30 @@
+package leets.weeth.domain.schedule.test.fixture;
+
+import java.time.LocalDateTime;
+
+import leets.weeth.domain.schedule.domain.entity.Event;
+import leets.weeth.domain.schedule.domain.entity.Meeting;
+
+public class ScheduleTestFixture {
+
+ public static Event createEvent() {
+ return Event.builder()
+ .title("Test Meeting")
+ .location("Test Location")
+ .start(LocalDateTime.now())
+ .end(LocalDateTime.now().plusDays(2))
+ .cardinal(1)
+ .build();
+ }
+
+ public static Meeting createMeeting() {
+ return Meeting.builder()
+ .title("Test Meeting")
+ .location("Test Location")
+ .start(LocalDateTime.now())
+ .end(LocalDateTime.now().plusDays(2))
+ .code(1234)
+ .cardinal(1)
+ .build();
+ }
+}
diff --git a/src/test/java/leets/weeth/domain/user/application/usecase/CardinalUseCaseTest.java b/src/test/java/leets/weeth/domain/user/application/usecase/CardinalUseCaseTest.java
new file mode 100644
index 000000000..316de92c4
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/user/application/usecase/CardinalUseCaseTest.java
@@ -0,0 +1,155 @@
+package leets.weeth.domain.user.application.usecase;
+
+import static org.assertj.core.api.AssertionsForClassTypes.*;
+import static org.assertj.core.api.InstanceOfAssertFactories.*;
+import static org.mockito.BDDMockito.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import leets.weeth.domain.user.application.dto.request.CardinalSaveRequest;
+import leets.weeth.domain.user.application.dto.request.CardinalUpdateRequest;
+import leets.weeth.domain.user.application.dto.response.CardinalResponse;
+import leets.weeth.domain.user.application.mapper.CardinalMapper;
+import leets.weeth.domain.user.domain.entity.Cardinal;
+import leets.weeth.domain.user.domain.entity.enums.CardinalStatus;
+import leets.weeth.domain.user.domain.service.CardinalGetService;
+import leets.weeth.domain.user.domain.service.CardinalSaveService;
+import leets.weeth.domain.user.test.fixture.CardinalTestFixture;
+
+@ExtendWith(MockitoExtension.class)
+public class CardinalUseCaseTest {
+ // ์ค์ CardinalUseCase์์ ์ฌ์ฉํ๋ ์์กด์ฑ์ Mock ๊ฐ์ฒด๋ก ๋์ ์ฃผ์
+ @Mock
+ private CardinalGetService cardinalGetService;
+
+ @Mock
+ private CardinalSaveService cardinalSaveService;
+
+ @Mock
+ private CardinalMapper cardinalMapper;
+
+ @InjectMocks
+ private CardinalUseCase useCase;
+
+ // Given-When-Then ํจํด์ ์ฝ๊ฒ ์ดํดํ๊ธฐ ์ํด ๋ฉ์๋๋ช
_์ํฉ_์์๊ฒฐ๊ณผ๋ก ํ
์คํธ ๋ฉ์๋ ๋ค์ด๋ฐ
+
+
+ @Test // ์งํ์ค์ด ์๋ ๊ธฐ์๋ฅผ ๋ฑ๋กํ๋ ๊ฒฝ์ฐ
+ void save_์งํ์ค์ด_์๋_๊ธฐ์๋ผ๋ฉด_๊ฒ์ฆํ_์ ์ฅ๋ง() {
+ //given
+ var request = new CardinalSaveRequest(7, 2025,1,false);
+
+ var toSave = CardinalTestFixture.createCardinal(7, 2025, 1);
+ var saved = CardinalTestFixture.createCardinal(7,2025 ,1);
+
+ willDoNothing().given(cardinalGetService).validateCardinal(7);
+ given(cardinalMapper.from(request)).willReturn(toSave);
+ given(cardinalSaveService.save(toSave)).willReturn(saved);
+
+ //when
+ useCase.save(request);
+
+ //then
+ then(cardinalGetService).should().validateCardinal(7);
+ then(cardinalSaveService).should().save(toSave);
+ then(cardinalGetService).should(never()).findInProgress();
+ }
+
+ @Test
+ void save_์_๊ธฐ์๊ฐ_์งํ์ค์ด๋ผ๋ฉด_๊ธฐ์กด_๊ธฐ์๋_done_ํ์ฌ๊ธฐ์๋_inProgress() {
+ // given
+ var request = new CardinalSaveRequest(7, 2025,1,true);
+
+ var oldCardinal = CardinalTestFixture.createCardinalInProgress(6, 2024, 2);
+ var newCardinalBeforeSave = CardinalTestFixture.createCardinal(7, 2025, 1);
+ var newCardinalAfterSave = CardinalTestFixture.createCardinal(7, 2025, 1);
+
+ given(cardinalGetService.findInProgress()).willReturn(List.of(oldCardinal));
+ given(cardinalMapper.from(request)).willReturn(newCardinalBeforeSave);
+ given(cardinalSaveService.save(newCardinalBeforeSave)).willReturn(newCardinalAfterSave);
+
+ // when
+ useCase.save(request);
+
+ // then
+ then(cardinalGetService).should().findInProgress();
+ then(cardinalSaveService).should().save(newCardinalBeforeSave);
+
+ assertThat(oldCardinal.getStatus()).isEqualTo(CardinalStatus.DONE);
+ assertThat(newCardinalAfterSave.getStatus()).isEqualTo(CardinalStatus.IN_PROGRESS);
+ }
+
+
+ @Test
+ void update_์ฐ๋์_ํ๊ธฐ๋ฅผ_๋ณ๊ฒฝํ๋ค() {
+ //given
+ var cardinal = CardinalTestFixture.createCardinal(6, 2024, 2);
+ var dto = new CardinalUpdateRequest(1L, 2025,1,false);
+
+ //when
+ cardinal.update(dto);
+
+ //then
+ assertThat(cardinal.getYear()).isEqualTo(2025);
+ assertThat(cardinal.getSemester()).isEqualTo(1);
+ }
+
+
+ @Test
+ void findAll_์กฐํ๋_๋ชจ๋ _๊ธฐ์๋ฅผ_DTO๋ก_๋งคํ์ฒ๋ฆฌ() {
+
+ //given
+ var cardinal1 = CardinalTestFixture.createCardinal(1L,6,2024,2);
+ var cardinal2 = CardinalTestFixture.createCardinalInProgress(2L,7,2025,1);
+ var cardinals = List.of(cardinal1, cardinal2);
+ var now = LocalDateTime.now();
+
+ var response1 = new CardinalResponse(
+ 1L, 6, 2024, 2,
+ CardinalStatus.DONE,
+ now.minusDays(5),
+ now.minusDays(3)
+ );
+
+ var response2 = new CardinalResponse(
+ 2L, 7, 2025, 1,
+ CardinalStatus.IN_PROGRESS,
+ now.minusDays(2),
+ now
+ );
+
+ given(cardinalGetService.findAll()).willReturn(cardinals);
+ given(cardinalMapper.to(cardinal1)).willReturn(response1);
+ given(cardinalMapper.to(cardinal2)).willReturn(response2);
+
+ //when
+ List responses = useCase.findAll();
+
+
+ //then
+ then(cardinalGetService).should().findAll();
+ then(cardinalMapper).should(times(2)).to(any(Cardinal.class));
+
+ assertThat(responses)
+ .asInstanceOf(list(CardinalResponse.class))
+ .hasSize(2)
+ .extracting(CardinalResponse::cardinalNumber)
+ .containsExactly(6, 7);
+
+ assertThat(responses)
+ .asInstanceOf(list(CardinalResponse.class))
+ .extracting(CardinalResponse::status)
+ .containsExactly(CardinalStatus.DONE, CardinalStatus.IN_PROGRESS);
+
+ assertThat(responses.get(0).createdAt()).isBefore(responses.get(1).createdAt());
+ }
+
+}
diff --git a/src/test/java/leets/weeth/domain/user/application/usecase/UserManageUseCaseTest.java b/src/test/java/leets/weeth/domain/user/application/usecase/UserManageUseCaseTest.java
new file mode 100644
index 000000000..0ab1e56de
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/user/application/usecase/UserManageUseCaseTest.java
@@ -0,0 +1,230 @@
+package leets.weeth.domain.user.application.usecase;
+
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.BDDMockito.*;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import leets.weeth.domain.attendance.domain.service.AttendanceSaveService;
+import leets.weeth.domain.schedule.domain.entity.Meeting;
+import leets.weeth.domain.schedule.domain.service.MeetingGetService;
+import leets.weeth.domain.user.application.dto.request.UserRequestDto;
+import leets.weeth.domain.user.application.dto.response.UserResponseDto;
+import leets.weeth.domain.user.application.exception.InvalidUserOrderException;
+import leets.weeth.domain.user.application.mapper.UserMapper;
+import leets.weeth.domain.user.domain.entity.Cardinal;
+import leets.weeth.domain.user.domain.entity.User;
+import leets.weeth.domain.user.domain.entity.UserCardinal;
+import leets.weeth.domain.user.domain.entity.enums.Role;
+import leets.weeth.domain.user.domain.entity.enums.Status;
+import leets.weeth.domain.user.domain.entity.enums.UsersOrderBy;
+import leets.weeth.domain.user.domain.service.CardinalGetService;
+import leets.weeth.domain.user.domain.service.UserCardinalGetService;
+import leets.weeth.domain.user.domain.service.UserCardinalSaveService;
+import leets.weeth.domain.user.domain.service.UserDeleteService;
+import leets.weeth.domain.user.domain.service.UserGetService;
+import leets.weeth.domain.user.domain.service.UserUpdateService;
+import leets.weeth.domain.user.test.fixture.CardinalTestFixture;
+import leets.weeth.domain.user.test.fixture.UserTestFixture;
+import leets.weeth.global.auth.jwt.service.JwtRedisService;
+
+@ExtendWith(MockitoExtension.class)
+public class UserManageUseCaseTest {
+
+ @Mock private UserGetService userGetService;
+ @Mock private UserUpdateService userUpdateService;
+ @Mock private UserDeleteService userDeleteService;
+
+ @Mock private AttendanceSaveService attendanceSaveService;
+ @Mock private MeetingGetService meetingGetService;
+ @Mock private JwtRedisService jwtRedisService;
+ @Mock private CardinalGetService cardinalGetService;
+ @Mock private UserCardinalSaveService userCardinalSaveService;
+ @Mock private UserCardinalGetService userCardinalGetService;
+
+ @Mock private UserMapper userMapper;
+ @Mock private PasswordEncoder passwordEncoder;
+
+ @InjectMocks
+ private UserManageUseCaseImpl useCase;
+
+
+ @Test
+ void findAllByAdmin_orderBy๊ฐ_null์ด๋ฉด_์์ธ๊ฐ์ ์๋ฐ์ํ๋์ง(){
+ //given
+ UsersOrderBy orderBy = null;
+
+ //when & then
+ assertThatThrownBy(() -> useCase.findAllByAdmin(orderBy))
+ .isInstanceOf(InvalidUserOrderException.class);
+
+ }
+
+ @Test
+ void findAllByAdmin์ด_orderBy์_๋ง๊ฒ_์ ๋ ฌ๋์ด_์กฐํ๋๋์ง() {
+ //given
+ UsersOrderBy orderBy = UsersOrderBy.NAME_ASCENDING;
+
+ var user1 = UserTestFixture.createActiveUser1();
+ var user2 = UserTestFixture.createWaitingUser2();
+ var cd1 = CardinalTestFixture.createCardinal(1L,6,2020,2);
+ var cd2 = CardinalTestFixture.createCardinal(2L,7,2021,1);
+ var uc1 = new UserCardinal(user1, cd1);
+ var uc2 = new UserCardinal(user2, cd2);
+
+ var adminResponse1 = new UserResponseDto.AdminResponse(
+ 1, "aaa", "a@a.com", "202034420", "01011112222", "์ฐ์
๊ณตํ๊ณผ",
+ List.of(6), null, Status.ACTIVE, null,
+ 0, 0, 0, 0, 0,
+ LocalDateTime.now().minusDays(3),
+ LocalDateTime.now()
+ );
+
+ var adminResponse2 = new UserResponseDto.AdminResponse(
+ 2, "bbb", "b@b.com", "202045678", "01033334444", "์ปดํจํฐ๊ณตํ๊ณผ",
+ List.of(7), null, Status.WAITING, null,
+ 0, 0, 0, 0, 0,
+ LocalDateTime.now().minusDays(2),
+ LocalDateTime.now()
+ );
+
+ given(userCardinalGetService.getUserCardinals(user1)).willReturn(List.of(uc1));
+ given(userCardinalGetService.getUserCardinals(user2)).willReturn(List.of(uc2));
+ given(userCardinalGetService.findAll()).willReturn(List.of(uc2, uc1));
+ given(userMapper.toAdminResponse(user1, List.of(uc1))).willReturn(adminResponse1);
+ given(userMapper.toAdminResponse(user2, List.of(uc2))).willReturn(adminResponse2);
+
+
+ //when
+ var result = useCase.findAllByAdmin(UsersOrderBy.NAME_ASCENDING);
+
+
+ //then
+ assertThat(result).hasSize(2);
+ assertThat(result.get(0).name()).isEqualTo("aaa");
+ assertThat(result.get(1).name()).isEqualTo("bbb");
+
+ }
+
+
+ @Test
+ void accept_๋นํ์ฑ์ ์ _์น์ธ์_์ถ์์ด๊ธฐํ_์ ์ํธ์ถ๋๋์ง() {
+ //given
+ var user1 = UserTestFixture.createWaitingUser1(1L);
+ var userIds = new UserRequestDto.UserId(List.of(1L));
+ var cardinal = CardinalTestFixture.createCardinal(1L,8,2020,2);
+ var meetings = List.of(mock(Meeting.class));
+
+ given(userGetService.findAll(userIds.userId())).willReturn(List.of(user1));
+ given(userCardinalGetService.getCurrentCardinal(user1)).willReturn(cardinal);
+ given(meetingGetService.find(8)).willReturn(meetings);
+
+ //when
+ useCase.accept(userIds);
+
+ //then
+ then(userUpdateService).should().accept(user1);
+ then(attendanceSaveService).should().init(user1,meetings);
+
+ }
+
+ @Test
+ void update_์ ์ ๊ถํ๋ณ๊ฒฝ์_DB์_Redis_๋ชจ๋๊ฐฑ์ ๋๋์ง() {
+ // given
+ var user1 = UserTestFixture.createActiveUser1(1L);
+ var request = new UserRequestDto.UserRoleUpdate(1L, Role.ADMIN);
+
+ lenient().when(userGetService.find((Long)1L)).thenReturn(user1);
+
+ // when
+ useCase.update(List.of(request));
+
+ // then
+ then(userUpdateService).should().update(user1, "ADMIN");
+ then(jwtRedisService).should().updateRole(1L, "ADMIN");
+ }
+
+ @Test
+ void leave_ํ์ํํด์_ํ ํฐ๋ฌดํจํ_๋ฐ_์ ์ ์ํ๋ณ๊ฒฝ๋๋์ง() {
+ //given
+ var user1 = UserTestFixture.createActiveUser1(1L);
+ given(userGetService.find((Long)1L)).willReturn(user1);
+
+ //when
+ useCase.leave(1L);
+
+ //then
+ then(jwtRedisService).should().delete(1L);
+ then(userDeleteService).should().leave(user1);
+
+ }
+
+ @Test
+ void ban_ํ์ban์_ํ ํฐ๋ฌดํจํ_๋ฐ_์ ์ ์ํ๋ณ๊ฒฝ๋๋์ง() {
+ //given
+ var user1 = UserTestFixture.createActiveUser1(1L);
+ var ids = new UserRequestDto.UserId(List.of(1L));
+ given(userGetService.findAll(ids.userId())).willReturn(List.of(user1));
+
+ //when
+ useCase.ban(ids);
+
+ //then
+ then(jwtRedisService).should().delete(1L);
+ then(userDeleteService).should().ban(user1);
+
+ }
+
+ @Test
+ void applyOB_ํ์ฌ๊ธฐ์_OB์ ์ฒญ์_์ถ์์ด๊ธฐํ_๋ฐ_๊ธฐ์์
๋ฐ์ดํธ() {
+ //given
+ var user = User.builder().id(1L).name("aaa").status(Status.ACTIVE).attendances(new ArrayList<>()).build();
+ var nextCardinal = CardinalTestFixture.createCardinal(1L,4,2020,2);
+ var request = new UserRequestDto.UserApplyOB(1L,4);
+ var meeting = List.of(mock(Meeting.class));
+
+ given(userGetService.find((Long)1L)).willReturn(user);
+ given(cardinalGetService.findByAdminSide(4)).willReturn(nextCardinal);
+ given(userCardinalGetService.notContains(user, nextCardinal)).willReturn(true);
+ given(userCardinalGetService.isCurrent(user, nextCardinal)).willReturn(true);
+ given(meetingGetService.find(4)).willReturn(meeting);
+
+ //when
+ useCase.applyOB(List.of(request));
+
+ //then
+ then(attendanceSaveService).should().init(user,meeting);
+ then(userCardinalSaveService).should().save(any(UserCardinal.class));
+ }
+
+ @Test
+ void reset_๋น๋ฐ๋ฒํธ์ด๊ธฐํ์_๋ชจ๋ ์ ์ ์_resetํธ์ถ๋๋์ง() {
+ // given
+ var user1 = UserTestFixture.createActiveUser1(1L);
+ var user2 = UserTestFixture.createActiveUser2(2L);
+ var ids = new UserRequestDto.UserId(List.of(1L, 2L));
+
+ given(userGetService.findAll(ids.userId())).willReturn(List.of(user1, user2));
+
+ // when
+ useCase.reset(ids);
+
+ // then
+ then(userGetService).should().findAll(ids.userId());
+ then(userUpdateService).should().reset(user1, passwordEncoder);
+ then(userUpdateService).should().reset(user2, passwordEncoder);
+ }
+
+}
diff --git a/src/test/java/leets/weeth/domain/user/domain/repository/CardinalRepositoryTest.java b/src/test/java/leets/weeth/domain/user/domain/repository/CardinalRepositoryTest.java
new file mode 100644
index 000000000..b773712dd
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/user/domain/repository/CardinalRepositoryTest.java
@@ -0,0 +1,38 @@
+package leets.weeth.domain.user.domain.repository;
+
+import static org.assertj.core.api.AssertionsForClassTypes.*;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.context.annotation.Import;
+
+import leets.weeth.config.TestContainersConfig;
+import leets.weeth.domain.user.domain.entity.Cardinal;
+import leets.weeth.domain.user.test.fixture.CardinalTestFixture;
+
+@DataJpaTest
+@Import(TestContainersConfig.class)
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+public class CardinalRepositoryTest {
+
+ @Autowired
+ CardinalRepository cardinalRepository;
+
+ @Test
+ void ๊ธฐ์๋ฒํธ๋ก_์กฐํ๋๋์ง() {
+ //given
+ var cardinal = CardinalTestFixture.createCardinal(7,2025,1);
+ cardinalRepository.save(cardinal);
+
+ //when
+ var result = cardinalRepository.findByCardinalNumber(7);
+
+ //then
+ assertThat(result).isPresent();
+ assertThat(result.get().getYear()).isEqualTo(2025);
+ }
+
+
+}
diff --git a/src/test/java/leets/weeth/domain/user/domain/repository/UserCardinalRepositoryTest.java b/src/test/java/leets/weeth/domain/user/domain/repository/UserCardinalRepositoryTest.java
new file mode 100644
index 000000000..34a9bafd5
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/user/domain/repository/UserCardinalRepositoryTest.java
@@ -0,0 +1,99 @@
+package leets.weeth.domain.user.domain.repository;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.context.annotation.Import;
+
+import leets.weeth.config.TestContainersConfig;
+import leets.weeth.domain.user.domain.entity.UserCardinal;
+import leets.weeth.domain.user.test.fixture.CardinalTestFixture;
+import leets.weeth.domain.user.test.fixture.UserTestFixture;
+
+@DataJpaTest
+@Import(TestContainersConfig.class)
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+public class UserCardinalRepositoryTest {
+
+ @Autowired
+ UserRepository userRepository;
+
+ @Autowired
+ CardinalRepository cardinalRepository;
+
+ @Autowired
+ UserCardinalRepository userCardinalRepository;
+
+ @Test
+ void ์ ์ ๋ณ_๊ธฐ์_๋ด๋ฆผ์ฐจ์_์กฐํ๋๋์ง() {
+ //given
+ var user = UserTestFixture.createActiveUser1();
+
+ userRepository.save(user);
+
+ var cardinal1 = cardinalRepository.save(CardinalTestFixture.createCardinal(5,2023,1));
+ var cardinal2 = cardinalRepository.save(CardinalTestFixture.createCardinal(6,2023,2));
+ var cardinal3 = cardinalRepository.save(CardinalTestFixture.createCardinal(7,2024,1));
+
+ userCardinalRepository.saveAll(List.of(
+ new UserCardinal(user, cardinal1),
+ new UserCardinal(user, cardinal2),
+ new UserCardinal(user, cardinal3)
+ ));
+
+ //when
+ List result = userCardinalRepository.findAllByUserOrderByCardinalCardinalNumberDesc(user);
+
+ //then
+ assertThat(result).hasSize(3);
+ assertThat(result.get(0).getCardinal().getCardinalNumber()).isEqualTo(7);
+ assertThat(result.get(1).getCardinal().getCardinalNumber()).isEqualTo(6);
+ assertThat(result.get(2).getCardinal().getCardinalNumber()).isEqualTo(5);
+
+ }
+
+ @Test
+ void ์ฌ๋ฌ_์ ์ ์_๊ธฐ์๋ฅผ_์ ์ ๋ณ_๋ด๋ฆผ์ฐจ์์ผ๋ก_์กฐํํ๋ค() {
+ //given
+ var user1 = UserTestFixture.createActiveUser1();
+ var user2 = UserTestFixture.createActiveUser2();
+
+ userRepository.save(user1);
+ userRepository.save(user2);
+
+ var c1 = cardinalRepository.save(CardinalTestFixture.createCardinal(5,2023,1));
+ var c2 = cardinalRepository.save(CardinalTestFixture.createCardinal(6,2023,2));
+ var c3 = cardinalRepository.save(CardinalTestFixture.createCardinal(7,2024,1));
+ var c4 = cardinalRepository.save(CardinalTestFixture.createCardinal(8,2024,2));
+
+ userCardinalRepository.saveAll(List.of(
+ new UserCardinal(user1, c3),
+ new UserCardinal(user1, c2)
+ ));
+ userCardinalRepository.saveAll(List.of(
+ new UserCardinal(user2, c4),
+ new UserCardinal(user2, c1)
+ ));
+
+ //when
+ List result = userCardinalRepository.findAllByUsers(List.of(user1, user2));
+
+ //then
+ assertThat(result).hasSize(4);
+ assertThat(result.get(0).getUser().getId()).isEqualTo(user1.getId());
+ assertThat(result.get(0).getCardinal().getCardinalNumber()).isEqualTo(7);
+ assertThat(result.get(1).getCardinal().getCardinalNumber()).isEqualTo(6);
+
+ assertThat(result.get(2).getUser().getId()).isEqualTo(user2.getId());
+ assertThat(result.get(2).getCardinal().getCardinalNumber()).isEqualTo(8);
+ assertThat(result.get(3).getCardinal().getCardinalNumber()).isEqualTo(5);
+ }
+
+
+}
diff --git a/src/test/java/leets/weeth/domain/user/domain/repository/UserRepositoryTest.java b/src/test/java/leets/weeth/domain/user/domain/repository/UserRepositoryTest.java
new file mode 100644
index 000000000..110efc5f6
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/user/domain/repository/UserRepositoryTest.java
@@ -0,0 +1,111 @@
+package leets.weeth.domain.user.domain.repository;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.*;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+
+import leets.weeth.config.TestContainersConfig;
+import leets.weeth.domain.user.domain.entity.Cardinal;
+import leets.weeth.domain.user.domain.entity.User;
+import leets.weeth.domain.user.domain.entity.UserCardinal;
+import leets.weeth.domain.user.domain.entity.enums.Status;
+import leets.weeth.domain.user.test.fixture.CardinalTestFixture;
+import leets.weeth.domain.user.test.fixture.UserCardinalTestFixture;
+import leets.weeth.domain.user.test.fixture.UserTestFixture;
+
+@DataJpaTest
+@Import(TestContainersConfig.class)
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+public class UserRepositoryTest {
+
+ @Autowired
+ private UserRepository userRepository;
+
+ @Autowired
+ private UserCardinalRepository userCardinalRepository;
+
+ @Autowired
+ private CardinalRepository cardinalRepository;
+
+ private Cardinal cardinal7;
+ private Cardinal cardinal8;
+
+ @BeforeEach
+ void setUp() {
+
+ cardinal7 = cardinalRepository.save(CardinalTestFixture.createCardinal(7, 2026, 1));
+ cardinal8 = cardinalRepository.save(CardinalTestFixture.createCardinal(8, 2026, 2));
+
+ var user1 = userRepository.save(UserTestFixture.createActiveUser1());
+ var user2 = userRepository.save(UserTestFixture.createActiveUser2());
+ var user3 = userRepository.save(UserTestFixture.createWaitingUser1());
+
+ user1.accept();
+ user2.accept();
+ userCardinalRepository.flush();
+
+ userCardinalRepository.save(UserCardinalTestFixture.linkUserCardinal(user1, cardinal7));
+ userCardinalRepository.save(UserCardinalTestFixture.linkUserCardinal(user2, cardinal8));
+ userCardinalRepository.save(UserCardinalTestFixture.linkUserCardinal(user3, cardinal7));
+
+ }
+
+ @Test
+ @DisplayName("findAllByCardinalAndStatus(): ํน์ ๊ธฐ์ + ์ํ์ ๋ง๋ ์ ์ ๋ง ์กฐํ๋๋ค")
+ void findAllByCardinalAndStatus() {
+ // when
+ List result = userRepository.findAllByCardinalAndStatus(cardinal7, Status.ACTIVE);
+
+ // then
+ assertThat(result)
+ .hasSize(1)
+ .extracting(User::getName)
+ .containsExactly("์ ์");
+ }
+
+ @Test
+ @DisplayName("findAllByStatusOrderByCardinalAndName() : ์ํ๋ณ๋ก ์ต์ ๊ธฐ์์ + ์ด๋ฆ ์ค๋ฆ์ฐจ์์ผ๋ก ์ ๋ ฌ๋๋ค")
+ void findAllByStatusOrderByCardinalAndName() {
+ //given
+ Pageable pageable = PageRequest.of(0,10);
+
+ //when
+ Slice resultSlice = userRepository.findAllByStatusOrderedByCardinalAndName(Status.ACTIVE, pageable);
+ List result = resultSlice.getContent();
+
+ //then
+ assertThat(result)
+ .hasSize(2)
+ .extracting(User::getName)
+ .containsExactly("์ ์2", "์ ์");
+ }
+
+ @Test
+ @DisplayName("findAllByCardinalOrderByNameAsc() : Active์ธ ์ ์ ๋ค ์ค ํน์ ๊ธฐ์ + ์ด๋ฆ ์ค๋ฆ์ฐจ์์ผ๋ก ์ ๋ ฌํ๋ค.")
+ void findAllByCardinalOrderByNameAsc() {
+ //given
+ Pageable pageable = PageRequest.of(0,10);
+
+ //when
+ Slice resultSlice = userRepository.findAllByCardinalOrderByNameAsc(Status.ACTIVE, cardinal7,pageable);
+ List result = resultSlice.getContent();
+
+ //then
+ assertThat(result)
+ .hasSize(1)
+ .extracting(User::getName)
+ .containsExactly("์ ์");
+ }
+
+}
diff --git a/src/test/java/leets/weeth/domain/user/domain/service/CardinalGetServiceTest.java b/src/test/java/leets/weeth/domain/user/domain/service/CardinalGetServiceTest.java
new file mode 100644
index 000000000..b564019da
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/user/domain/service/CardinalGetServiceTest.java
@@ -0,0 +1,70 @@
+package leets.weeth.domain.user.domain.service;
+
+import static org.assertj.core.api.AssertionsForClassTypes.*;
+import static org.mockito.BDDMockito.*;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import leets.weeth.domain.user.application.exception.DuplicateCardinalException;
+import leets.weeth.domain.user.domain.entity.Cardinal;
+import leets.weeth.domain.user.domain.repository.CardinalRepository;
+
+@ExtendWith(MockitoExtension.class)
+public class CardinalGetServiceTest {
+
+ // validateCardinal() ๊ฒ์ฆ๋ก์ง ํ์ธ
+ @Mock
+ private CardinalRepository cardinalRepository;
+
+ @InjectMocks
+ private CardinalGetService cardinalGetService;
+
+ @Test
+ @DisplayName("findByAdminSide() : ์กด์ฌํ์ง ์๋ ๊ธฐ์๋ฅผ ๋ฃ์์ ๋ ์๋ก ์ ์ฅ๋๋์ง ํ์ธ")
+ void findByAdminSide() {
+ //given
+ given(cardinalRepository.findByCardinalNumber(7))
+ .willReturn(Optional.empty());
+
+ given(cardinalRepository.save(any(Cardinal.class))).
+ willReturn(Cardinal.builder().cardinalNumber(7).build());
+
+ //when
+ Cardinal result = cardinalGetService.findByAdminSide(7);
+
+ //then
+ assertThat(result.getCardinalNumber()).isEqualTo(7);
+ }
+
+ @Test
+ @DisplayName("validateCardinal() : ์ค๋ณต๋ ๊ธฐ์ ์ ์ฅ์ ๋ฐฉ์งํ๊ณ ์์ธ๋ฅผ ๋์ง๋์ง ํ์ธ")
+ void validateCardinal() {
+ //given
+ given(cardinalRepository.findByCardinalNumber(7))
+ .willReturn(Optional.of(Cardinal.builder().cardinalNumber(7).build()));
+
+ //when&then
+ assertThatThrownBy(() -> cardinalGetService.validateCardinal(7))
+ .isInstanceOf(DuplicateCardinalException.class);
+ }
+
+ @Test
+ @DisplayName("validateCardinal() : ์ค๋ณต๋์ง ์๋ ๊ธฐ์๋ผ๋ฉด ์์ธ๋ฅผ ๋์ง์ง์๊ณ ์ ์ ์ฅํ๋์ง ํ์ธ")
+ void validateCardinal_noException() {
+ //given
+ given(cardinalRepository.findByCardinalNumber(7))
+ .willReturn(Optional.empty());
+
+ //when&then
+ assertThatCode(() -> cardinalGetService.validateCardinal(7))
+ .doesNotThrowAnyException();
+ }
+
+}
diff --git a/src/test/java/leets/weeth/domain/user/domain/service/UserCardinalGetServiceTest.java b/src/test/java/leets/weeth/domain/user/domain/service/UserCardinalGetServiceTest.java
new file mode 100644
index 000000000..c6e303817
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/user/domain/service/UserCardinalGetServiceTest.java
@@ -0,0 +1,103 @@
+package leets.weeth.domain.user.domain.service;
+
+import static org.assertj.core.api.AssertionsForClassTypes.*;
+import static org.mockito.BDDMockito.*;
+
+import java.util.List;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import leets.weeth.domain.user.application.exception.CardinalNotFoundException;
+import leets.weeth.domain.user.domain.entity.UserCardinal;
+import leets.weeth.domain.user.domain.repository.UserCardinalRepository;
+import leets.weeth.domain.user.test.fixture.CardinalTestFixture;
+import leets.weeth.domain.user.test.fixture.UserTestFixture;
+
+@ExtendWith(MockitoExtension.class)
+public class UserCardinalGetServiceTest {
+
+ @Mock
+ private UserCardinalRepository userCardinalRepository;
+
+ @InjectMocks
+ private UserCardinalGetService userCardinalGetService;
+
+
+ @Test
+ @DisplayName("notContains() : ์ ์ ์ ๊ธฐ์ ๋ชฉ๋ก ์ค, ํน์ ๊ธฐ์๊ฐ ์๋์ง ํ์ธ ")
+ void notContains() {
+ //given
+ var user = UserTestFixture.createActiveUser1();
+ var existingCardinal = CardinalTestFixture.createCardinal(7,2025,2);
+ var targetCardinal = CardinalTestFixture.createCardinal(8,2026,1);
+ var userCardinal = new UserCardinal(user, existingCardinal);
+
+ given(userCardinalRepository.findAllByUserOrderByCardinalCardinalNumberDesc(user))
+ .willReturn(List.of(userCardinal));
+
+ //when
+ boolean result = userCardinalGetService.notContains(user, targetCardinal);
+
+ //then
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ @DisplayName("isCurrent() : ํ์ฌ ์ ์ ์ ์ต์ ๊ธฐ์๋ณด๋ค ์ต์ ๊ธฐ์์ธ์ง ํ์ธ")
+ void isCurrent() {
+ //given
+ var user = UserTestFixture.createActiveUser1();
+ var oldCardinal = CardinalTestFixture.createCardinal(7,2025,2);
+ var newCardinal = CardinalTestFixture.createCardinal(8,2026,1);
+ var userCardinal = new UserCardinal(user, oldCardinal);
+
+ given(userCardinalRepository.findAllByUserOrderByCardinalCardinalNumberDesc(user))
+ .willReturn(List.of(userCardinal));
+
+ //when
+ boolean result = userCardinalGetService.isCurrent(user, newCardinal);
+
+ //then
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ @DisplayName("isCurrent(): ์ ๊ธฐ์๊ฐ ๊ธฐ์กด ์ต๋๋ณด๋ค ์์ผ๋ฉด false ๋ฐํ")
+ void isCurrent_returnsFalse_whenOlderCardinal() {
+ // given
+ var user = UserTestFixture.createActiveUser1();
+ var oldCardinal = CardinalTestFixture.createCardinal(7, 2025, 1);
+ var newCardinal = CardinalTestFixture.createCardinal(6, 2024, 2);
+ var userCardinal = new UserCardinal(user, oldCardinal);
+
+ given(userCardinalRepository.findAllByUserOrderByCardinalCardinalNumberDesc(user))
+ .willReturn(List.of(userCardinal));
+
+ // when
+ boolean result = userCardinalGetService.isCurrent(user, newCardinal);
+
+ // then
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ @DisplayName("isCurrent(): ์ ์ ๊ฐ ์ด๋ค ๊ธฐ์๋ ๊ฐ์ง๊ณ ์์ง ์์ผ๋ฉด CardinalNotFoundException ๋ฐ์")
+ void isCurrent_throwsException_whenUserHasNoCardinal() {
+ // given
+ var user = UserTestFixture.createActiveUser1();
+ var newCardinal = CardinalTestFixture.createCardinal(8, 2026, 1);
+
+ given(userCardinalRepository.findAllByUserOrderByCardinalCardinalNumberDesc(user))
+ .willReturn(List.of());
+
+ // when & then
+ assertThatThrownBy(() -> userCardinalGetService.isCurrent(user, newCardinal))
+ .isInstanceOf(CardinalNotFoundException.class);
+ }
+
+}
diff --git a/src/test/java/leets/weeth/domain/user/domain/service/UserGetServiceTest.java b/src/test/java/leets/weeth/domain/user/domain/service/UserGetServiceTest.java
new file mode 100644
index 000000000..36877c2b8
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/user/domain/service/UserGetServiceTest.java
@@ -0,0 +1,72 @@
+package leets.weeth.domain.user.domain.service;
+
+import leets.weeth.domain.user.application.exception.UserNotFoundException;
+import leets.weeth.domain.user.domain.entity.User;
+import leets.weeth.domain.user.domain.repository.UserRepository;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.data.domain.SliceImpl;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.BDDMockito.given;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@ExtendWith(MockitoExtension.class)
+class UserGetServiceTest {
+
+ @Mock
+ private UserRepository userRepository;
+
+ @InjectMocks
+ private UserGetService userGetService;
+
+ @Test
+ @DisplayName("find(Long Id) : ์กด์ฌํ์ง ์๋ ์ ์ ์ผ ๋ ์์ธ๋ฅผ ๋์ง๋ค")
+ void find_id_userNotFound_throwsException() {
+ //given
+ Long userId = 1L;
+ given(userRepository.findById(userId)).willReturn(Optional.empty());
+
+ // when & then
+ assertThrows(UserNotFoundException.class, () -> userGetService.find(userId));
+ }
+
+ @Test
+ @DisplayName("find(String email) : ์กด์ฌํ์ง ์๋ ์ ์ ์ผ ๋ ์์ธ๋ฅผ ๋์ง๋ค")
+ void find_email_userNotFound_throwsException() {
+ //given
+ String email = "test@test.com";
+ given(userRepository.findByEmail(email)).willReturn(Optional.empty());
+
+ //when & then
+ assertThrows(UserNotFoundException.class, () -> userGetService.find(email));
+ }
+
+ @Test
+ @DisplayName("findAll(Pageable pageable) : ๋น ์ฌ๋ผ์ด์ค ๋ฐํ ์ , ์ ์ ์์ธ ๋์ง๋ค")
+ void findAll_userNotFound_throwsException() {
+ //given
+ Pageable pageable = PageRequest.of(0, 10);
+ Slice emptySlice = new SliceImpl<>(List.of(), pageable, false);
+
+ given(userRepository.findAllByStatusOrderedByCardinalAndName(any(), eq(pageable)))
+ .willReturn(emptySlice);
+
+ //when & then
+ assertThrows(UserNotFoundException.class,
+ () -> userGetService.findAll(pageable));
+
+ }
+
+}
diff --git a/src/test/java/leets/weeth/domain/user/test/fixture/CardinalTestFixture.java b/src/test/java/leets/weeth/domain/user/test/fixture/CardinalTestFixture.java
new file mode 100644
index 000000000..ecb713f41
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/user/test/fixture/CardinalTestFixture.java
@@ -0,0 +1,45 @@
+package leets.weeth.domain.user.test.fixture;
+
+import leets.weeth.domain.user.domain.entity.Cardinal;
+import leets.weeth.domain.user.domain.entity.enums.CardinalStatus;
+
+public class CardinalTestFixture {
+
+ public static Cardinal createCardinal(int cardinalNumber, int year, int semester) {
+ return Cardinal.builder()
+ .cardinalNumber(cardinalNumber)
+ .year(year)
+ .semester(semester)
+ .status(CardinalStatus.DONE)
+ .build();
+ }
+
+ public static Cardinal createCardinal(Long id, int cardinalNumber, int year, int semester) {
+ return Cardinal.builder()
+ .id(id)
+ .cardinalNumber(cardinalNumber)
+ .year(year)
+ .semester(semester)
+ .status(CardinalStatus.DONE)
+ .build();
+ }
+
+ public static Cardinal createCardinalInProgress ( int cardinalNumber, int year, int semester) {
+ return Cardinal.builder()
+ .cardinalNumber(cardinalNumber)
+ .year(year)
+ .semester(semester)
+ .status(CardinalStatus.IN_PROGRESS)
+ .build();
+ }
+
+ public static Cardinal createCardinalInProgress(Long id, int cardinalNumber, int year, int semester) {
+ return Cardinal.builder()
+ .id(id)
+ .cardinalNumber(cardinalNumber)
+ .year(year)
+ .semester(semester)
+ .status(CardinalStatus.IN_PROGRESS)
+ .build();
+ }
+}
diff --git a/src/test/java/leets/weeth/domain/user/test/fixture/UserCardinalTestFixture.java b/src/test/java/leets/weeth/domain/user/test/fixture/UserCardinalTestFixture.java
new file mode 100644
index 000000000..686031daf
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/user/test/fixture/UserCardinalTestFixture.java
@@ -0,0 +1,12 @@
+package leets.weeth.domain.user.test.fixture;
+
+import leets.weeth.domain.user.domain.entity.Cardinal;
+import leets.weeth.domain.user.domain.entity.User;
+import leets.weeth.domain.user.domain.entity.UserCardinal;
+
+public class UserCardinalTestFixture {
+
+ public static UserCardinal linkUserCardinal(User user, Cardinal cardinal) {
+ return new UserCardinal(user, cardinal);
+ }
+}
diff --git a/src/test/java/leets/weeth/domain/user/test/fixture/UserTestFixture.java b/src/test/java/leets/weeth/domain/user/test/fixture/UserTestFixture.java
new file mode 100644
index 000000000..200092104
--- /dev/null
+++ b/src/test/java/leets/weeth/domain/user/test/fixture/UserTestFixture.java
@@ -0,0 +1,76 @@
+package leets.weeth.domain.user.test.fixture;
+
+import leets.weeth.domain.user.domain.entity.User;
+import leets.weeth.domain.user.domain.entity.enums.Status;
+
+public class UserTestFixture {
+
+ public static User createActiveUser1() {
+ return User.builder()
+ .name("์ ์")
+ .email("test1@test.com")
+ .status(Status.ACTIVE)
+ .build();
+ }
+
+ public static User createActiveUser1(Long id) {
+ return User.builder()
+ .id(id)
+ .name("์ ์")
+ .email("test1@test.com")
+ .status(Status.ACTIVE)
+ .build();
+ }
+
+ public static User createActiveUser2() {
+ return User.builder()
+ .name("์ ์2")
+ .email("test2@test.com")
+ .status(Status.ACTIVE)
+ .build();
+ }
+
+ public static User createActiveUser2(Long id) {
+ return User.builder()
+ .id(id)
+ .name("์ ์2")
+ .email("test2@test.com")
+ .status(Status.ACTIVE)
+ .build();
+ }
+
+ public static User createWaitingUser1() {
+ return User.builder()
+ .name("์์ ")
+ .email("test2@test.com")
+ .status(Status.WAITING)
+ .build();
+ }
+
+ public static User createWaitingUser1(Long id) {
+ return User.builder()
+ .id(id)
+ .name("์์ ")
+ .email("test2@test.com")
+ .status(Status.WAITING)
+ .build();
+ }
+
+ public static User createWaitingUser2() {
+ return User.builder()
+ .name("์์ 2")
+ .email("test3@test.com")
+ .status(Status.WAITING)
+ .build();
+ }
+
+ public static User createWaitingUser2(Long id) {
+ return User.builder()
+ .id(id)
+ .name("์์ 2")
+ .email("test3@test.com")
+ .status(Status.WAITING)
+ .build();
+ }
+
+}
diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
new file mode 100644
index 000000000..4747e28c4
--- /dev/null
+++ b/src/test/resources/application-test.yml
@@ -0,0 +1,11 @@
+spring:
+ profiles:
+ active: test
+ jpa:
+ hibernate:
+ ddl-auto: create-drop
+ show-sql: true
+ properties:
+ hibernate:
+ format_sql: true
+ dialect: org.hibernate.dialect.MySQL8Dialect