From 31ca4b349f315b6c5f16e5db5774e735e4b18697 Mon Sep 17 00:00:00 2001 From: pia01190 Date: Mon, 21 Jul 2025 23:39:08 +0900 Subject: [PATCH 01/18] =?UTF-8?q?[EA3-168]=20feature:=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EC=8B=A0=EC=B2=AD=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/service/RecruitmentPostService.java | 6 +- .../repository/StudyMemberRepository.java | 3 + .../studyapplication/StudyApplication.java | 4 +- .../controller/ApplicationController.java | 10 +++- .../controller/ApplicationSpecification.java | 2 +- .../dto/request/ApplicationCreateRequest.java | 32 +++++++++++ .../exception/code/ApplicationErrorCode.java | 21 +++++++ ...sitory.java => ApplicationRepository.java} | 4 +- .../service/ApplicationService.java | 55 +++++++++++++++++++ .../global/auth/entity/RefreshToken.java | 3 - .../service/RecruitmentPostServiceTest.java | 6 +- 11 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java rename src/main/java/grep/neogul_coder/domain/studyapplication/repository/{StudyApplicationRepository.java => ApplicationRepository.java} (63%) create mode 100644 src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java diff --git a/src/main/java/grep/neogul_coder/domain/recruitment/post/service/RecruitmentPostService.java b/src/main/java/grep/neogul_coder/domain/recruitment/post/service/RecruitmentPostService.java index af49d8b8..a73cd268 100644 --- a/src/main/java/grep/neogul_coder/domain/recruitment/post/service/RecruitmentPostService.java +++ b/src/main/java/grep/neogul_coder/domain/recruitment/post/service/RecruitmentPostService.java @@ -15,7 +15,7 @@ import grep.neogul_coder.domain.study.Study; import grep.neogul_coder.domain.study.repository.StudyRepository; import grep.neogul_coder.domain.studyapplication.StudyApplication; -import grep.neogul_coder.domain.studyapplication.repository.StudyApplicationRepository; +import grep.neogul_coder.domain.studyapplication.repository.ApplicationRepository; import grep.neogul_coder.global.exception.business.BusinessException; import grep.neogul_coder.global.exception.business.NotFoundException; import lombok.RequiredArgsConstructor; @@ -40,7 +40,7 @@ public class RecruitmentPostService { private final RecruitmentPostRepository postRepository; private final RecruitmentPostQueryRepository postQueryRepository; - private final StudyApplicationRepository studyApplicationRepository; + private final ApplicationRepository applicationRepository; private final RecruitmentPostCommentQueryRepository commentQueryRepository; public RecruitmentPostInfo get(long recruitmentPostId) { @@ -49,7 +49,7 @@ public RecruitmentPostInfo get(long recruitmentPostId) { RecruitmentPostWithStudyInfo postInfo = postQueryRepository.findPostWithStudyInfo(post.getId()); List comments = findCommentsWithWriterInfo(post); - List applications = studyApplicationRepository.findByRecruitmentPostId(post.getId()); + List applications = applicationRepository.findByRecruitmentPostId(post.getId()); return new RecruitmentPostInfo(postInfo, comments, applications.size()); } diff --git a/src/main/java/grep/neogul_coder/domain/study/repository/StudyMemberRepository.java b/src/main/java/grep/neogul_coder/domain/study/repository/StudyMemberRepository.java index 239a7fe8..f00bc2f6 100644 --- a/src/main/java/grep/neogul_coder/domain/study/repository/StudyMemberRepository.java +++ b/src/main/java/grep/neogul_coder/domain/study/repository/StudyMemberRepository.java @@ -2,6 +2,7 @@ import grep.neogul_coder.domain.study.Study; import grep.neogul_coder.domain.study.StudyMember; +import grep.neogul_coder.domain.study.enums.StudyMemberRole; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -37,4 +38,6 @@ public interface StudyMemberRepository extends JpaRepository LocalDateTime findCreatedDateByStudyIdAndUserId(@Param("studyId") Long studyId, @Param("userId") Long userId); boolean existsByStudyIdAndUserId(Long studyId, Long id); + + boolean existsByStudyIdAndUserIdAndRole(Long studyId, Long userId, StudyMemberRole role); } diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/StudyApplication.java b/src/main/java/grep/neogul_coder/domain/studyapplication/StudyApplication.java index 3c9dea1e..7cbbecde 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/StudyApplication.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/StudyApplication.java @@ -3,13 +3,15 @@ import grep.neogul_coder.global.entity.BaseEntity; import jakarta.persistence.*; import lombok.Builder; +import lombok.Getter; +@Getter @Entity public class StudyApplication extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long studyApplicationId; + private Long id; @Column(nullable = false) private Long recruitmentPostId; diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java index af2b0279..7d90c281 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java @@ -2,24 +2,30 @@ import grep.neogul_coder.domain.studyapplication.controller.dto.request.ApplicationCreateRequest; import grep.neogul_coder.domain.studyapplication.controller.dto.response.MyApplicationResponse; +import grep.neogul_coder.domain.studyapplication.service.ApplicationService; import grep.neogul_coder.global.response.ApiResponse; import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.List; @RequestMapping("/api/applications") +@RequiredArgsConstructor @RestController public class ApplicationController implements ApplicationSpecification { + private final ApplicationService applicationService; + @GetMapping("/me") public ApiResponse> getMyStudyApplications() { return ApiResponse.success(List.of(new MyApplicationResponse())); } @PostMapping - public ApiResponse createApplication(@RequestBody @Valid ApplicationCreateRequest request) { - return ApiResponse.noContent(); + public ApiResponse createApplication(@RequestBody @Valid ApplicationCreateRequest request) { + Long id = applicationService.createApplication(request); + return ApiResponse.success(id); } @PostMapping("/{applicationId}/approve") diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java index 57d9f4c8..bbebd7ba 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java @@ -15,7 +15,7 @@ public interface ApplicationSpecification { ApiResponse> getMyStudyApplications(); @Operation(summary = "스터디 신청 생성", description = "스터디를 신청합니다.") - ApiResponse createApplication(ApplicationCreateRequest request); + ApiResponse createApplication(ApplicationCreateRequest request); @Operation(summary = "스터디 신청 승인", description = "스터디장이 스터디 신청을 승인합니다.") ApiResponse approveApplication(Long applicationId); diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/request/ApplicationCreateRequest.java b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/request/ApplicationCreateRequest.java index fb634caa..fa678377 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/request/ApplicationCreateRequest.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/request/ApplicationCreateRequest.java @@ -1,13 +1,45 @@ package grep.neogul_coder.domain.studyapplication.controller.dto.request; +import grep.neogul_coder.domain.recruitment.post.RecruitmentPost; +import grep.neogul_coder.domain.studyapplication.ApplicationStatus; +import grep.neogul_coder.domain.studyapplication.StudyApplication; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; import lombok.Getter; @Getter public class ApplicationCreateRequest { + @Schema(description = "모집글 번호", example = "1") + private Long recruitmentPostId; + + @NotNull + @Schema(description = "유저 번호", example = "1") + private Long userId; + @NotBlank @Schema(description = "스터디 신청 지원 동기", example = "자바를 더 공부하고싶어 지원하였습니다.") private String applicationReason; + + private ApplicationCreateRequest() { + } + + @Builder + private ApplicationCreateRequest(Long recruitmentPostId, Long userId, String applicationReason) { + this.recruitmentPostId = recruitmentPostId; + this.userId = userId; + this.applicationReason = applicationReason; + } + + public StudyApplication toEntity() { + return StudyApplication.builder() + .recruitmentPostId(this.recruitmentPostId) + .userId(this.userId) + .applicationReason(this.applicationReason) + .isRead(false) + .status(ApplicationStatus.APPLYING) + .build(); + } } diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java b/src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java new file mode 100644 index 00000000..e9975152 --- /dev/null +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java @@ -0,0 +1,21 @@ +package grep.neogul_coder.domain.studyapplication.exception.code; + +import grep.neogul_coder.global.response.code.ErrorCode; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum ApplicationErrorCode implements ErrorCode { + ALREADY_APPLICATION("SA001", HttpStatus.BAD_REQUEST, "이미 지원한 모집글입니다."), + LEADER_CANNOT_APPLY("SA002", HttpStatus.BAD_REQUEST, "스터디장은 스터디를 신청할 수 없습니다."); + + private final String code; + private final HttpStatus status; + private final String message; + + ApplicationErrorCode(String code, HttpStatus status, String message) { + this.code = code; + this.status = status; + this.message = message; + } +} diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/repository/StudyApplicationRepository.java b/src/main/java/grep/neogul_coder/domain/studyapplication/repository/ApplicationRepository.java similarity index 63% rename from src/main/java/grep/neogul_coder/domain/studyapplication/repository/StudyApplicationRepository.java rename to src/main/java/grep/neogul_coder/domain/studyapplication/repository/ApplicationRepository.java index 25c6de3f..aefa42ff 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/repository/StudyApplicationRepository.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/repository/ApplicationRepository.java @@ -5,6 +5,8 @@ import java.util.List; -public interface StudyApplicationRepository extends JpaRepository { +public interface ApplicationRepository extends JpaRepository { List findByRecruitmentPostId(Long recruitmentPostId); + + boolean existsByRecruitmentPostIdAndUserId(Long recruitmentPostId, Long userId); } diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java b/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java new file mode 100644 index 00000000..037a09f2 --- /dev/null +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java @@ -0,0 +1,55 @@ +package grep.neogul_coder.domain.studyapplication.service; + +import grep.neogul_coder.domain.recruitment.post.RecruitmentPost; +import grep.neogul_coder.domain.recruitment.post.repository.RecruitmentPostRepository; +import grep.neogul_coder.domain.study.enums.StudyMemberRole; +import grep.neogul_coder.domain.study.repository.StudyMemberRepository; +import grep.neogul_coder.domain.studyapplication.StudyApplication; +import grep.neogul_coder.domain.studyapplication.controller.dto.request.ApplicationCreateRequest; +import grep.neogul_coder.domain.studyapplication.repository.ApplicationRepository; +import grep.neogul_coder.global.exception.business.BusinessException; +import grep.neogul_coder.global.exception.business.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static grep.neogul_coder.domain.recruitment.RecruitmentErrorCode.*; +import static grep.neogul_coder.domain.studyapplication.exception.code.ApplicationErrorCode.*; + +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class ApplicationService { + + private final ApplicationRepository applicationRepository; + private final RecruitmentPostRepository recruitmentPostRepository; + private final StudyMemberRepository studyMemberRepository; + + @Transactional + public Long createApplication(ApplicationCreateRequest request) { + RecruitmentPost recruitmentPost = recruitmentPostRepository.findByIdAndActivatedTrue(request.getRecruitmentPostId()) + .orElseThrow(() -> new NotFoundException(NOT_FOUND)); + + validateNotLeader(request, recruitmentPost); + validateNotAlreadyApplied(request); + + StudyApplication application = request.toEntity(); + applicationRepository.save(application); + + return application.getId(); + } + + private void validateNotLeader(ApplicationCreateRequest request, RecruitmentPost recruitmentPost) { + boolean isLeader = studyMemberRepository.existsByStudyIdAndUserIdAndRole(recruitmentPost.getStudyId(), request.getUserId(), StudyMemberRole.LEADER); + if (isLeader) { + throw new BusinessException(LEADER_CANNOT_APPLY); + } + } + + private void validateNotAlreadyApplied(ApplicationCreateRequest request) { + boolean alreadyApplied = applicationRepository.existsByRecruitmentPostIdAndUserId(request.getRecruitmentPostId(), request.getUserId()); + if (alreadyApplied) { + throw new BusinessException(ALREADY_APPLICATION); + } + } +} diff --git a/src/main/java/grep/neogul_coder/global/auth/entity/RefreshToken.java b/src/main/java/grep/neogul_coder/global/auth/entity/RefreshToken.java index 600e5090..0d36c8d0 100644 --- a/src/main/java/grep/neogul_coder/global/auth/entity/RefreshToken.java +++ b/src/main/java/grep/neogul_coder/global/auth/entity/RefreshToken.java @@ -12,9 +12,6 @@ public class RefreshToken { private String token = UUID.randomUUID().toString(); private Long ttl = 3600 * 24 * 7L; - public RefreshToken() { - } - public RefreshToken(String atId){ this.atId = atId; } diff --git a/src/test/java/grep/neogul_coder/domain/recruitment/post/service/RecruitmentPostServiceTest.java b/src/test/java/grep/neogul_coder/domain/recruitment/post/service/RecruitmentPostServiceTest.java index 35584190..e2b4827c 100644 --- a/src/test/java/grep/neogul_coder/domain/recruitment/post/service/RecruitmentPostServiceTest.java +++ b/src/test/java/grep/neogul_coder/domain/recruitment/post/service/RecruitmentPostServiceTest.java @@ -19,7 +19,7 @@ import grep.neogul_coder.domain.study.repository.StudyMemberRepository; import grep.neogul_coder.domain.study.repository.StudyRepository; import grep.neogul_coder.domain.studyapplication.StudyApplication; -import grep.neogul_coder.domain.studyapplication.repository.StudyApplicationRepository; +import grep.neogul_coder.domain.studyapplication.repository.ApplicationRepository; import grep.neogul_coder.domain.users.entity.User; import grep.neogul_coder.domain.users.repository.UserRepository; import grep.neogul_coder.global.exception.business.BusinessException; @@ -62,7 +62,7 @@ class RecruitmentPostServiceTest extends IntegrationTestSupport { private RecruitmentPostCommentRepository commentRepository; @Autowired - private StudyApplicationRepository studyApplicationRepository; + private ApplicationRepository applicationRepository; private long userId; private long recruitmentPostId; @@ -99,7 +99,7 @@ void get() { StudyApplication application1 = createStudyApplication(post.getId(), user1.getId(), "신청 사유"); StudyApplication application2 = createStudyApplication(post.getId(), user2.getId(), "신청 사유2"); - studyApplicationRepository.saveAll(List.of(application1, application2)); + applicationRepository.saveAll(List.of(application1, application2)); //when RecruitmentPostInfo response = recruitmentPostService.get(post.getId()); From 23b7b08ff143de7b1777c7d72fa5c4029558174e Mon Sep 17 00:00:00 2001 From: pia01190 Date: Tue, 22 Jul 2025 00:17:22 +0900 Subject: [PATCH 02/18] =?UTF-8?q?[EA3-168]=20feature:=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EC=8B=A0=EC=B2=AD=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ApplicationController.java | 8 +-- .../controller/ApplicationSpecification.java | 3 +- .../dto/response/MyApplicationResponse.java | 25 ++++++++- .../ApplicationQueryRepository.java | 52 +++++++++++++++++++ .../service/ApplicationService.java | 14 ++++- 5 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 src/main/java/grep/neogul_coder/domain/studyapplication/repository/ApplicationQueryRepository.java diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java index 7d90c281..15f629c4 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java @@ -3,9 +3,11 @@ import grep.neogul_coder.domain.studyapplication.controller.dto.request.ApplicationCreateRequest; import grep.neogul_coder.domain.studyapplication.controller.dto.response.MyApplicationResponse; import grep.neogul_coder.domain.studyapplication.service.ApplicationService; +import grep.neogul_coder.global.auth.Principal; import grep.neogul_coder.global.response.ApiResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -17,9 +19,9 @@ public class ApplicationController implements ApplicationSpecification { private final ApplicationService applicationService; - @GetMapping("/me") - public ApiResponse> getMyStudyApplications() { - return ApiResponse.success(List.of(new MyApplicationResponse())); + @GetMapping + public ApiResponse> getMyStudyApplications(@AuthenticationPrincipal Principal userDetails) { + return ApiResponse.success(applicationService.getMyStudyApplications(userDetails.getUserId())); } @PostMapping diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java index bbebd7ba..fa073a09 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java @@ -2,6 +2,7 @@ import grep.neogul_coder.domain.studyapplication.controller.dto.request.ApplicationCreateRequest; import grep.neogul_coder.domain.studyapplication.controller.dto.response.MyApplicationResponse; +import grep.neogul_coder.global.auth.Principal; import grep.neogul_coder.global.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -12,7 +13,7 @@ public interface ApplicationSpecification { @Operation(summary = "내 스터디 신청 목록 조회", description = "내가 신청한 스터디의 목록을 조회합니다.") - ApiResponse> getMyStudyApplications(); + ApiResponse> getMyStudyApplications(Principal userDetails); @Operation(summary = "스터디 신청 생성", description = "스터디를 신청합니다.") ApiResponse createApplication(ApplicationCreateRequest request); diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/response/MyApplicationResponse.java b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/response/MyApplicationResponse.java index 2b7d62b1..a4f388e8 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/response/MyApplicationResponse.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/response/MyApplicationResponse.java @@ -1,16 +1,20 @@ package grep.neogul_coder.domain.studyapplication.controller.dto.response; +import com.querydsl.core.annotations.QueryProjection; import grep.neogul_coder.domain.study.enums.Category; import grep.neogul_coder.domain.study.enums.StudyType; import grep.neogul_coder.domain.studyapplication.ApplicationStatus; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; -import java.time.LocalDate; +import java.time.LocalDateTime; @Getter public class MyApplicationResponse { + @Schema(description = "신청 번호", example = "1") + private Long applicationId; + @Schema(description = "스터디 이름", example = "자바 스터디") private String name; @@ -24,7 +28,7 @@ public class MyApplicationResponse { private int currentCount; @Schema(description = "시작일", example = "2025-07-15") - private LocalDate startDate; + private LocalDateTime startDate; @Schema(description = "대표 이미지", example = "http://localhost:8083/image.jpg") private String imageUrl; @@ -43,4 +47,21 @@ public class MyApplicationResponse { @Schema(description = "신청 상태", example = "PENDING") private ApplicationStatus status; + + @QueryProjection + public MyApplicationResponse(Long applicationId, String name, String leaderNickname, int capacity, int currentCount, LocalDateTime startDate, + String imageUrl,String introduction, Category category, StudyType studyType, boolean isRead, ApplicationStatus status) { + this.applicationId = applicationId; + this.name = name; + this.leaderNickname = leaderNickname; + this.capacity = capacity; + this.currentCount = currentCount; + this.startDate = startDate; + this.imageUrl = imageUrl; + this.introduction = introduction; + this.category = category; + this.studyType = studyType; + this.isRead = isRead; + this.status = status; + } } diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/repository/ApplicationQueryRepository.java b/src/main/java/grep/neogul_coder/domain/studyapplication/repository/ApplicationQueryRepository.java new file mode 100644 index 00000000..231cad1b --- /dev/null +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/repository/ApplicationQueryRepository.java @@ -0,0 +1,52 @@ +package grep.neogul_coder.domain.studyapplication.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import grep.neogul_coder.domain.study.enums.StudyMemberRole; +import grep.neogul_coder.domain.studyapplication.controller.dto.response.MyApplicationResponse; +import grep.neogul_coder.domain.studyapplication.controller.dto.response.QMyApplicationResponse; +import jakarta.persistence.EntityManager; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static grep.neogul_coder.domain.recruitment.post.QRecruitmentPost.recruitmentPost; +import static grep.neogul_coder.domain.study.QStudy.study; +import static grep.neogul_coder.domain.study.QStudyMember.studyMember; +import static grep.neogul_coder.domain.study.enums.StudyMemberRole.*; +import static grep.neogul_coder.domain.studyapplication.QStudyApplication.studyApplication; +import static grep.neogul_coder.domain.users.entity.QUser.user; + +@Repository +public class ApplicationQueryRepository { + + private final JPAQueryFactory queryFactory; + + public ApplicationQueryRepository(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + public List findMyApplications(Long userId) { + return queryFactory + .select(new QMyApplicationResponse( + studyApplication.id, + study.name, + user.nickname, + study.capacity, + study.currentCount, + study.startDate, + study.imageUrl, + study.introduction, + study.category, + study.studyType, + studyApplication.isRead, + studyApplication.status + )) + .from(studyApplication) + .join(recruitmentPost).on(recruitmentPost.id.eq(studyApplication.recruitmentPostId)) + .join(study).on(study.id.eq(recruitmentPost.studyId)) + .join(studyMember).on(studyMember.study.id.eq(study.id), studyMember.role.eq(LEADER)) + .join(user).on(user.id.eq(studyMember.userId)) + .where(studyApplication.userId.eq(userId)) + .fetch(); + } +} diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java b/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java index 037a09f2..84a2c477 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java @@ -6,6 +6,8 @@ import grep.neogul_coder.domain.study.repository.StudyMemberRepository; import grep.neogul_coder.domain.studyapplication.StudyApplication; import grep.neogul_coder.domain.studyapplication.controller.dto.request.ApplicationCreateRequest; +import grep.neogul_coder.domain.studyapplication.controller.dto.response.MyApplicationResponse; +import grep.neogul_coder.domain.studyapplication.repository.ApplicationQueryRepository; import grep.neogul_coder.domain.studyapplication.repository.ApplicationRepository; import grep.neogul_coder.global.exception.business.BusinessException; import grep.neogul_coder.global.exception.business.NotFoundException; @@ -13,8 +15,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import static grep.neogul_coder.domain.recruitment.RecruitmentErrorCode.*; -import static grep.neogul_coder.domain.studyapplication.exception.code.ApplicationErrorCode.*; +import java.util.List; + +import static grep.neogul_coder.domain.recruitment.RecruitmentErrorCode.NOT_FOUND; +import static grep.neogul_coder.domain.studyapplication.exception.code.ApplicationErrorCode.ALREADY_APPLICATION; +import static grep.neogul_coder.domain.studyapplication.exception.code.ApplicationErrorCode.LEADER_CANNOT_APPLY; @Transactional(readOnly = true) @RequiredArgsConstructor @@ -22,9 +27,14 @@ public class ApplicationService { private final ApplicationRepository applicationRepository; + private final ApplicationQueryRepository applicationQueryRepository; private final RecruitmentPostRepository recruitmentPostRepository; private final StudyMemberRepository studyMemberRepository; + public List getMyStudyApplications(Long userId) { + return applicationQueryRepository.findMyApplications(userId); + } + @Transactional public Long createApplication(ApplicationCreateRequest request) { RecruitmentPost recruitmentPost = recruitmentPostRepository.findByIdAndActivatedTrue(request.getRecruitmentPostId()) From 6cfb996b5d993c6f2eee5f9490358cb14b0519f1 Mon Sep 17 00:00:00 2001 From: endorsement0912 Date: Tue, 22 Jul 2025 13:20:10 +0900 Subject: [PATCH 03/18] =?UTF-8?q?[EA3-134]=20chore:=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EB=AA=A8=EC=9E=84=EC=8B=9C=EA=B0=84=20=EC=A1=B0?= =?UTF-8?q?=EC=9C=A8=20-=20service=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/timevote/controller/TimeVoteController.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/grep/neogul_coder/domain/timevote/controller/TimeVoteController.java b/src/main/java/grep/neogul_coder/domain/timevote/controller/TimeVoteController.java index 8bde0482..93d5f837 100644 --- a/src/main/java/grep/neogul_coder/domain/timevote/controller/TimeVoteController.java +++ b/src/main/java/grep/neogul_coder/domain/timevote/controller/TimeVoteController.java @@ -8,7 +8,6 @@ import grep.neogul_coder.domain.timevote.dto.response.TimeVoteResponse; import grep.neogul_coder.domain.timevote.dto.response.TimeVoteStatListResponse; import grep.neogul_coder.domain.timevote.dto.response.TimeVoteSubmissionStatusResponse; -import grep.neogul_coder.domain.timevote.service.TimeVotePeriodService; import grep.neogul_coder.global.auth.Principal; import grep.neogul_coder.global.response.ApiResponse; import jakarta.validation.Valid; @@ -30,8 +29,6 @@ @RequestMapping("/api/studies/{studyId}/time-vote") public class TimeVoteController implements TimeVoteSpecification { - private final TimeVotePeriodService timeVotePeriodService; - @PostMapping("/periods") public ApiResponse createPeriod( @PathVariable("studyId") Long studyId, From 271f29e8d91fdac7a27df424cd18bc820904a32f Mon Sep 17 00:00:00 2001 From: pia01190 Date: Tue, 22 Jul 2025 14:37:18 +0900 Subject: [PATCH 04/18] =?UTF-8?q?[EA3-168]=20feature:=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EC=8B=A0=EC=B2=AD=20=EC=8A=B9=EC=9D=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grep/neogul_coder/domain/study/Study.java | 4 ++ .../domain/study/StudyMember.java | 8 +++ .../studyapplication/StudyApplication.java | 4 ++ .../controller/ApplicationController.java | 4 +- .../controller/ApplicationSpecification.java | 2 +- .../exception/code/ApplicationErrorCode.java | 9 ++- .../service/ApplicationService.java | 64 +++++++++++++++++-- 7 files changed, 85 insertions(+), 10 deletions(-) diff --git a/src/main/java/grep/neogul_coder/domain/study/Study.java b/src/main/java/grep/neogul_coder/domain/study/Study.java index fb69e252..7ccf711c 100644 --- a/src/main/java/grep/neogul_coder/domain/study/Study.java +++ b/src/main/java/grep/neogul_coder/domain/study/Study.java @@ -86,6 +86,10 @@ public long calculateRemainSlots(long currentCount) { return this.capacity - currentCount; } + public void increaseMemberCount() { + currentCount++; + } + public void decreaseMemberCount() { currentCount--; } diff --git a/src/main/java/grep/neogul_coder/domain/study/StudyMember.java b/src/main/java/grep/neogul_coder/domain/study/StudyMember.java index 66e92859..38260637 100644 --- a/src/main/java/grep/neogul_coder/domain/study/StudyMember.java +++ b/src/main/java/grep/neogul_coder/domain/study/StudyMember.java @@ -36,6 +36,14 @@ public StudyMember(Study study, Long userId, StudyMemberRole role) { this.participated = false; } + public static StudyMember createMember(Study study, Long userId) { + return StudyMember.builder() + .study(study) + .userId(userId) + .role(StudyMemberRole.MEMBER) + .build(); + } + public void delete() { this.activated = false; } diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/StudyApplication.java b/src/main/java/grep/neogul_coder/domain/studyapplication/StudyApplication.java index 7cbbecde..441ac727 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/StudyApplication.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/StudyApplication.java @@ -37,4 +37,8 @@ private StudyApplication(Long recruitmentPostId, String applicationReason, Appli } protected StudyApplication() {} + + public void approve() { + this.status = ApplicationStatus.APPROVED; + } } diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java index 15f629c4..4bb2be7f 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java @@ -31,7 +31,9 @@ public ApiResponse createApplication(@RequestBody @Valid ApplicationCreate } @PostMapping("/{applicationId}/approve") - public ApiResponse approveApplication(@PathVariable("applicationId") Long applicationId) { + public ApiResponse approveApplication(@PathVariable("applicationId") Long applicationId, + @AuthenticationPrincipal Principal userDetails) { + applicationService.approveApplication(applicationId, userDetails.getUserId()); return ApiResponse.noContent(); } diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java index fa073a09..864ee860 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java @@ -19,7 +19,7 @@ public interface ApplicationSpecification { ApiResponse createApplication(ApplicationCreateRequest request); @Operation(summary = "스터디 신청 승인", description = "스터디장이 스터디 신청을 승인합니다.") - ApiResponse approveApplication(Long applicationId); + ApiResponse approveApplication(Long applicationId, Principal userDetails); @Operation(summary = "스터디 신청 거절", description = "스터디장이 스터디 신청을 거절합니다.") ApiResponse rejectApplication(Long applicationId); diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java b/src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java index e9975152..4a8fa907 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java @@ -6,8 +6,13 @@ @Getter public enum ApplicationErrorCode implements ErrorCode { - ALREADY_APPLICATION("SA001", HttpStatus.BAD_REQUEST, "이미 지원한 모집글입니다."), - LEADER_CANNOT_APPLY("SA002", HttpStatus.BAD_REQUEST, "스터디장은 스터디를 신청할 수 없습니다."); + APPLICATION_NOT_FOUND("SA001",HttpStatus.NOT_FOUND,"신청서를 찾을 수 없습니다."), + + ALREADY_APPLICATION("SA002", HttpStatus.BAD_REQUEST, "이미 지원한 모집글입니다."), + ALREADY_APPROVED("SA003", HttpStatus.BAD_REQUEST, "이미 승인된 신청입니다."), + + LEADER_CANNOT_APPLY("SA004", HttpStatus.BAD_REQUEST, "스터디장은 스터디를 신청할 수 없습니다."), + LEADER_ONLY_APPROVED("SA005", HttpStatus.BAD_REQUEST, "스터디장만 승인이 가능합니다."); private final String code; private final HttpStatus status; diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java b/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java index 84a2c477..f41cff98 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java @@ -2,8 +2,12 @@ import grep.neogul_coder.domain.recruitment.post.RecruitmentPost; import grep.neogul_coder.domain.recruitment.post.repository.RecruitmentPostRepository; +import grep.neogul_coder.domain.study.Study; +import grep.neogul_coder.domain.study.StudyMember; import grep.neogul_coder.domain.study.enums.StudyMemberRole; import grep.neogul_coder.domain.study.repository.StudyMemberRepository; +import grep.neogul_coder.domain.study.repository.StudyRepository; +import grep.neogul_coder.domain.studyapplication.ApplicationStatus; import grep.neogul_coder.domain.studyapplication.StudyApplication; import grep.neogul_coder.domain.studyapplication.controller.dto.request.ApplicationCreateRequest; import grep.neogul_coder.domain.studyapplication.controller.dto.response.MyApplicationResponse; @@ -18,8 +22,8 @@ import java.util.List; import static grep.neogul_coder.domain.recruitment.RecruitmentErrorCode.NOT_FOUND; -import static grep.neogul_coder.domain.studyapplication.exception.code.ApplicationErrorCode.ALREADY_APPLICATION; -import static grep.neogul_coder.domain.studyapplication.exception.code.ApplicationErrorCode.LEADER_CANNOT_APPLY; +import static grep.neogul_coder.domain.study.exception.code.StudyErrorCode.STUDY_NOT_FOUND; +import static grep.neogul_coder.domain.studyapplication.exception.code.ApplicationErrorCode.*; @Transactional(readOnly = true) @RequiredArgsConstructor @@ -30,6 +34,7 @@ public class ApplicationService { private final ApplicationQueryRepository applicationQueryRepository; private final RecruitmentPostRepository recruitmentPostRepository; private final StudyMemberRepository studyMemberRepository; + private final StudyRepository studyRepository; public List getMyStudyApplications(Long userId) { return applicationQueryRepository.findMyApplications(userId); @@ -37,10 +42,9 @@ public List getMyStudyApplications(Long userId) { @Transactional public Long createApplication(ApplicationCreateRequest request) { - RecruitmentPost recruitmentPost = recruitmentPostRepository.findByIdAndActivatedTrue(request.getRecruitmentPostId()) - .orElseThrow(() -> new NotFoundException(NOT_FOUND)); + RecruitmentPost recruitmentPost = findValidRecruimentPost(request.getRecruitmentPostId()); - validateNotLeader(request, recruitmentPost); + validateNotLeaderApply(request, recruitmentPost); validateNotAlreadyApplied(request); StudyApplication application = request.toEntity(); @@ -49,7 +53,42 @@ public Long createApplication(ApplicationCreateRequest request) { return application.getId(); } - private void validateNotLeader(ApplicationCreateRequest request, RecruitmentPost recruitmentPost) { + @Transactional + public void approveApplication(Long applicationId, Long userId) { + StudyApplication application = findValidApplication(applicationId); + RecruitmentPost post = findValidRecruimentPost(application.getRecruitmentPostId()); + Study study = findValidStudy(post); + + validateOnlyLeaderCanApproved(study, userId); + validateAlreadyApproved(application); + + application.approve(); + + StudyMember studyMember = StudyMember.createMember(study, application.getUserId()); + studyMemberRepository.save(studyMember); + + study.increaseMemberCount(); + } + + private Study findValidStudy(RecruitmentPost post) { + Study study = studyRepository.findByIdAndActivatedTrue(post.getStudyId()) + .orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND)); + return study; + } + + private StudyApplication findValidApplication(Long applicationId) { + StudyApplication application = applicationRepository.findById(applicationId) + .orElseThrow(() -> new NotFoundException(APPLICATION_NOT_FOUND)); + return application; + } + + private RecruitmentPost findValidRecruimentPost(Long applicationId) { + RecruitmentPost post = recruitmentPostRepository.findByIdAndActivatedTrue(applicationId) + .orElseThrow(() -> new NotFoundException(NOT_FOUND)); + return post; + } + + private void validateNotLeaderApply(ApplicationCreateRequest request, RecruitmentPost recruitmentPost) { boolean isLeader = studyMemberRepository.existsByStudyIdAndUserIdAndRole(recruitmentPost.getStudyId(), request.getUserId(), StudyMemberRole.LEADER); if (isLeader) { throw new BusinessException(LEADER_CANNOT_APPLY); @@ -62,4 +101,17 @@ private void validateNotAlreadyApplied(ApplicationCreateRequest request) { throw new BusinessException(ALREADY_APPLICATION); } } + + private static void validateAlreadyApproved(StudyApplication application) { + if (application.getStatus() == ApplicationStatus.APPROVED) { + throw new BusinessException(ALREADY_APPROVED); + } + } + + private void validateOnlyLeaderCanApproved(Study study, Long userId) { + boolean isLeader = studyMemberRepository.existsByStudyIdAndUserIdAndRole(study.getId(), userId, StudyMemberRole.LEADER); + if (!isLeader) { + throw new BusinessException(LEADER_ONLY_APPROVED); + } + } } From 1a812701c87143069545efe2740129eb041c95ab Mon Sep 17 00:00:00 2001 From: hyeunS-P Date: Tue, 22 Jul 2025 14:58:43 +0900 Subject: [PATCH 05/18] =?UTF-8?q?[EA3-173]=20feature:=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20PR=20=ED=85=9C?= =?UTF-8?q?=ED=94=8C=EB=A6=BF=20=EC=9E=90=EB=8F=99=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=EC=97=90=20OAuth=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../users/controller/UserController.java | 28 +++++--- .../controller/dto/response/UserResponse.java | 9 ++- .../exception/UnActivatedUserException.java | 11 +++ .../users/exception/code/UserErrorCode.java | 3 +- .../domain/users/service/UserService.java | 23 +++--- .../global/auth/service/AuthService.java | 71 +++++++++++-------- .../global/response/ApiResponse.java | 4 ++ 7 files changed, 97 insertions(+), 52 deletions(-) create mode 100644 src/main/java/grep/neogul_coder/domain/users/exception/UnActivatedUserException.java diff --git a/src/main/java/grep/neogul_coder/domain/users/controller/UserController.java b/src/main/java/grep/neogul_coder/domain/users/controller/UserController.java index 63ddb2c4..55c6b3c9 100644 --- a/src/main/java/grep/neogul_coder/domain/users/controller/UserController.java +++ b/src/main/java/grep/neogul_coder/domain/users/controller/UserController.java @@ -8,15 +8,23 @@ import grep.neogul_coder.global.auth.Principal; import grep.neogul_coder.global.response.ApiResponse; import jakarta.validation.Valid; +import java.io.IOException; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; - @RestController @RequiredArgsConstructor @RequestMapping("/api/users") @@ -38,9 +46,9 @@ public ApiResponse get(@PathVariable("userid") Long userId) { @PutMapping(value = "/update/profile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ApiResponse updateProfile( - @AuthenticationPrincipal Principal principal, - @RequestPart("nickname") String nickname, - @RequestPart(value = "profileImage", required = false) MultipartFile profileImage + @AuthenticationPrincipal Principal principal, + @RequestPart("nickname") String nickname, + @RequestPart(value = "profileImage", required = false) MultipartFile profileImage ) throws IOException { usersService.updateProfile(principal.getUserId(), nickname, profileImage); return ApiResponse.noContent(); @@ -48,14 +56,15 @@ public ApiResponse updateProfile( @PutMapping("/update/password") public ApiResponse updatePassword(@AuthenticationPrincipal Principal principal, - @Valid @RequestBody UpdatePasswordRequest request) { - usersService.updatePassword(principal.getUserId(), request.getPassword(), request.getNewPassword(), request.getNewPasswordCheck()); + @Valid @RequestBody UpdatePasswordRequest request) { + usersService.updatePassword(principal.getUserId(), request.getPassword(), + request.getNewPassword(), request.getNewPasswordCheck()); return ApiResponse.noContent(); } @DeleteMapping("/delete/me") public ApiResponse delete(@AuthenticationPrincipal Principal principal, - @RequestBody @Valid PasswordRequest request) { + @RequestBody @Valid PasswordRequest request) { usersService.deleteUser(principal.getUserId(), request.getPassword()); return ApiResponse.noContent(); } @@ -66,5 +75,4 @@ public ApiResponse signUp(@Valid @RequestBody SignUpRequest request) { usersService.signUp(request); return ApiResponse.noContent(); } - } \ No newline at end of file diff --git a/src/main/java/grep/neogul_coder/domain/users/controller/dto/response/UserResponse.java b/src/main/java/grep/neogul_coder/domain/users/controller/dto/response/UserResponse.java index 38fa5de5..4ce853d4 100644 --- a/src/main/java/grep/neogul_coder/domain/users/controller/dto/response/UserResponse.java +++ b/src/main/java/grep/neogul_coder/domain/users/controller/dto/response/UserResponse.java @@ -20,24 +20,29 @@ public class UserResponse { @Schema(description = "회원 프로필 이미지", example = "profileImageUrl") private String profileImageUrl; + @Schema(description = "회원 OAuth 정보", example = "Google") + private String oauth; + @Schema(description = "Role") private Role role; @Builder - private UserResponse(Long id, String email, String nickname,String profileImageUrl, Role role) { + private UserResponse(Long id, String email, String nickname,String profileImageUrl, String oauth ,Role role) { this.id = id; this.email = email; this.nickname = nickname; this.profileImageUrl = profileImageUrl; + this.oauth = oauth; this.role = role; } - public static UserResponse toUserResponse(Long id, String email, String nickname,String profileImageUrl, Role role){ + public static UserResponse toUserResponse(Long id, String email, String nickname,String profileImageUrl, String oauth, Role role){ return UserResponse.builder() .id(id) .email(email) .nickname(nickname) .profileImageUrl(profileImageUrl) + .oauth(oauth) .role(role) .build(); } diff --git a/src/main/java/grep/neogul_coder/domain/users/exception/UnActivatedUserException.java b/src/main/java/grep/neogul_coder/domain/users/exception/UnActivatedUserException.java new file mode 100644 index 00000000..62114177 --- /dev/null +++ b/src/main/java/grep/neogul_coder/domain/users/exception/UnActivatedUserException.java @@ -0,0 +1,11 @@ +package grep.neogul_coder.domain.users.exception; + +import grep.neogul_coder.global.exception.business.BusinessException; +import grep.neogul_coder.global.response.code.ErrorCode; + +public class UnActivatedUserException extends BusinessException { + + public UnActivatedUserException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/grep/neogul_coder/domain/users/exception/code/UserErrorCode.java b/src/main/java/grep/neogul_coder/domain/users/exception/code/UserErrorCode.java index 9958e581..e868689d 100644 --- a/src/main/java/grep/neogul_coder/domain/users/exception/code/UserErrorCode.java +++ b/src/main/java/grep/neogul_coder/domain/users/exception/code/UserErrorCode.java @@ -11,7 +11,8 @@ public enum UserErrorCode implements ErrorCode { PASSWORD_MISMATCH("U002", HttpStatus.BAD_REQUEST, "비밀번호를 다시 확인해주세요."), PASSWORD_UNCHECKED("U003", HttpStatus.BAD_REQUEST, "비밀번호와 비밀번호 확인이 다릅니다"), IS_DUPLICATED_MALI("U004", HttpStatus.BAD_REQUEST,"이미 존재하는 이메일입니다."), - IS_DUPLICATED_NICKNAME("U005", HttpStatus.BAD_REQUEST,"이미 존재하는 닉네임입니다."); + IS_DUPLICATED_NICKNAME("U005", HttpStatus.BAD_REQUEST,"이미 존재하는 닉네임입니다."), + UNACTIVATED_USER("U006", HttpStatus.BAD_REQUEST,"탈퇴된 회원입니다."); private final String code; diff --git a/src/main/java/grep/neogul_coder/domain/users/service/UserService.java b/src/main/java/grep/neogul_coder/domain/users/service/UserService.java index 644e0b77..6b2e319f 100644 --- a/src/main/java/grep/neogul_coder/domain/users/service/UserService.java +++ b/src/main/java/grep/neogul_coder/domain/users/service/UserService.java @@ -22,6 +22,7 @@ import grep.neogul_coder.global.utils.upload.uploader.GcpFileUploader; import grep.neogul_coder.global.utils.upload.uploader.LocalFileUploader; import jakarta.transaction.Transactional; +import jakarta.validation.constraints.NotBlank; import java.io.IOException; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; @@ -78,14 +79,7 @@ public void signUp(SignUpRequest request) { User user = findUser(request.getEmail()); - prTemplateRepository.save( - PrTemplate.PrTemplateInit(user.getId(), null, null)); - - linkRepository.save(Link.LinkInit(user.getId(), null, null)); - linkRepository.save(Link.LinkInit(user.getId(), null, null)); - - // 회원가입 시 버디 에너지 +50 생성 - buddyEnergyService.createDefaultEnergy(user.getId()); + initializeUserData(user.getId()); } @Transactional @@ -93,7 +87,9 @@ public void updateProfile(Long userId, String nickname, MultipartFile profileIma throws IOException { User user = findUser(userId); - isDuplicateNickname(nickname); + if(isDuplicateNickname(nickname)){ + throw new NicknameDuplicatedException(UserErrorCode.IS_DUPLICATED_NICKNAME); + } String uploadedImageUrl; if (isProfileImgExists(profileImage)) { @@ -147,6 +143,13 @@ public void deleteUser(Long userId) { user.delete(); } + public void initializeUserData(Long userId) { + prTemplateRepository.save(PrTemplate.PrTemplateInit(userId, null, null)); + linkRepository.save(Link.LinkInit(userId, null, null)); + linkRepository.save(Link.LinkInit(userId, null, null)); + buddyEnergyService.createDefaultEnergy(userId); + } + public UserResponse getUserResponse(Long userId) { User user = get(userId); return UserResponse.toUserResponse( @@ -154,6 +157,7 @@ public UserResponse getUserResponse(Long userId) { user.getEmail(), user.getNickname(), user.getProfileImageUrl(), + user.getOauthProvider(), user.getRole()); } @@ -211,7 +215,6 @@ private boolean isProductionEnvironment() { private boolean isProfileImgExists(MultipartFile profileImage) { return profileImage != null && !profileImage.isEmpty(); } - } diff --git a/src/main/java/grep/neogul_coder/global/auth/service/AuthService.java b/src/main/java/grep/neogul_coder/global/auth/service/AuthService.java index 332917bc..a13b3510 100644 --- a/src/main/java/grep/neogul_coder/global/auth/service/AuthService.java +++ b/src/main/java/grep/neogul_coder/global/auth/service/AuthService.java @@ -1,7 +1,10 @@ package grep.neogul_coder.global.auth.service; import grep.neogul_coder.domain.users.entity.User; +import grep.neogul_coder.domain.users.exception.UnActivatedUserException; +import grep.neogul_coder.domain.users.exception.code.UserErrorCode; import grep.neogul_coder.domain.users.repository.UserRepository; +import grep.neogul_coder.domain.users.service.UserService; import grep.neogul_coder.global.auth.code.Role; import grep.neogul_coder.global.auth.entity.RefreshToken; import grep.neogul_coder.global.auth.jwt.JwtTokenProvider; @@ -12,6 +15,7 @@ import grep.neogul_coder.global.auth.repository.UserBlackListRepository; import grep.neogul_coder.global.exception.GoogleUserLoginException; import grep.neogul_coder.global.response.code.CommonCode; +import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -23,8 +27,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.UUID; - @Service @RequiredArgsConstructor @Slf4j @@ -36,6 +38,7 @@ public class AuthService { private final RefreshTokenService refreshTokenService; private final UserBlackListRepository userBlackListRepository; private final UserRepository usersRepository; + private final UserService userService; public TokenDto signin(LoginRequest loginRequest) { @@ -43,18 +46,26 @@ public TokenDto signin(LoginRequest loginRequest) { throw new GoogleUserLoginException(CommonCode.SECURITY_INCIDENT); } + if (isUnactivatedUser(loginRequest.getEmail())) { + throw new UnActivatedUserException(UserErrorCode.UNACTIVATED_USER); + } + UsernamePasswordAuthenticationToken authenticationToken = - new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), - loginRequest.getPassword()); + new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), + loginRequest.getPassword()); Authentication authentication = authenticationManagerBuilder.getObject() - .authenticate(authenticationToken); + .authenticate(authenticationToken); SecurityContextHolder.getContext().setAuthentication(authentication); String roles = String.join(",", authentication.getAuthorities().stream().map( - GrantedAuthority::getAuthority).toList()); + GrantedAuthority::getAuthority).toList()); return processTokenSignin(authentication.getName(), roles); } + private boolean isUnactivatedUser(String email) { + return !userService.getByEmail(email).getActivated(); + } + public TokenDto processTokenSignin(String email, String roles) { userBlackListRepository.deleteByEmail(email); @@ -63,40 +74,42 @@ public TokenDto processTokenSignin(String email, String roles) { RefreshToken refreshToken = refreshTokenService.saveWithAtId(accessToken.getJti()); return TokenDto.builder() - .atId(accessToken.getJti()) - .accessToken(accessToken.getToken()) - .refreshToken(refreshToken.getToken()) - .grantType("Bearer") - .refreshExpiresIn(jwtTokenProvider.getRefreshTokenExpiration()) - .expiresIn(jwtTokenProvider.getAccessTokenExpiration()) - .build(); + .atId(accessToken.getJti()) + .accessToken(accessToken.getToken()) + .refreshToken(refreshToken.getToken()) + .grantType("Bearer") + .refreshExpiresIn(jwtTokenProvider.getRefreshTokenExpiration()) + .expiresIn(jwtTokenProvider.getAccessTokenExpiration()) + .build(); } @Transactional public TokenDto processOAuthSignin(OAuth2UserInfo userInfo, String roles) { String email = userInfo.getEmail(); - String dummyPassword = UUID.randomUUID().toString(); - User user = usersRepository.findByEmail(email) - .orElseGet(() -> { - User newUser = User.builder() - .email(email) - .nickname(userInfo.getName()) - .oauthProvider(userInfo.getProvider()) - .oauthId(userInfo.getProviderId()) - .password(dummyPassword) - .role(Role.ROLE_USER) - .build(); - return usersRepository.save(newUser); - }); - - return processTokenSignin(user.getEmail(), user.getRole().name()); + return usersRepository.findByEmail(email) + .map(user -> processTokenSignin(user.getEmail(), user.getRole().name())) + .orElseGet(() -> { + User newUser = User.builder() + .email(email) + .nickname(userInfo.getName()) + .oauthProvider(userInfo.getProvider()) + .oauthId(userInfo.getProviderId()) + .password(dummyPassword) + .role(Role.ROLE_USER) + .build(); + + User savedUser = usersRepository.save(newUser); + userService.initializeUserData(savedUser.getId()); + + return processTokenSignin(savedUser.getEmail(), savedUser.getRole().name()); + }); } private boolean isGoogleUser(String email) { User user = usersRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException("해당 이메일의 유저가 없습니다.")); + .orElseThrow(() -> new UsernameNotFoundException("해당 이메일의 유저가 없습니다.")); return "Google".equals(user.getOauthProvider()); } diff --git a/src/main/java/grep/neogul_coder/global/response/ApiResponse.java b/src/main/java/grep/neogul_coder/global/response/ApiResponse.java index 41d6c731..4656675a 100644 --- a/src/main/java/grep/neogul_coder/global/response/ApiResponse.java +++ b/src/main/java/grep/neogul_coder/global/response/ApiResponse.java @@ -25,6 +25,10 @@ public static ApiResponse success(T data) { return ApiResponse.of(CommonCode.OK.getCode(), CommonCode.OK.getMessage(), data); } + public static ApiResponse success(String message) { + return ApiResponse.of(CommonCode.OK.getCode(), message, null); + } + public static ApiResponse successWithCode(T data){ return ApiResponse.of(CommonCode.OK.getCode(), CommonCode.OK.getMessage(), data); } From 9be32bac83a2e04fb0ae7392ba1b7b619212f1ea Mon Sep 17 00:00:00 2001 From: pia01190 Date: Tue, 22 Jul 2025 15:11:46 +0900 Subject: [PATCH 06/18] =?UTF-8?q?[EA3-168]=20feature:=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EC=8B=A0=EC=B2=AD=20=EA=B1=B0=EC=A0=88=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyapplication/StudyApplication.java | 4 +++ .../controller/ApplicationController.java | 4 ++- .../controller/ApplicationSpecification.java | 2 +- .../exception/code/ApplicationErrorCode.java | 5 ++-- .../service/ApplicationService.java | 28 +++++++++++++++---- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/StudyApplication.java b/src/main/java/grep/neogul_coder/domain/studyapplication/StudyApplication.java index 441ac727..c74abb86 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/StudyApplication.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/StudyApplication.java @@ -41,4 +41,8 @@ protected StudyApplication() {} public void approve() { this.status = ApplicationStatus.APPROVED; } + + public void reject() { + this.status = ApplicationStatus.REJECTED; + } } diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java index 4bb2be7f..c241173d 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java @@ -38,7 +38,9 @@ public ApiResponse approveApplication(@PathVariable("applicationId") Long } @PostMapping("/{applicationId}/reject") - public ApiResponse rejectApplication(@PathVariable("applicationId") Long applicationId) { + public ApiResponse rejectApplication(@PathVariable("applicationId") Long applicationId, + @AuthenticationPrincipal Principal userDetails) { + applicationService.rejectApplication(applicationId, userDetails.getUserId()); return ApiResponse.noContent(); } } diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java index 864ee860..e2f17b4f 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java @@ -22,5 +22,5 @@ public interface ApplicationSpecification { ApiResponse approveApplication(Long applicationId, Principal userDetails); @Operation(summary = "스터디 신청 거절", description = "스터디장이 스터디 신청을 거절합니다.") - ApiResponse rejectApplication(Long applicationId); + ApiResponse rejectApplication(Long applicationId, Principal userDetails); } diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java b/src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java index 4a8fa907..2ad44295 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java @@ -9,10 +9,11 @@ public enum ApplicationErrorCode implements ErrorCode { APPLICATION_NOT_FOUND("SA001",HttpStatus.NOT_FOUND,"신청서를 찾을 수 없습니다."), ALREADY_APPLICATION("SA002", HttpStatus.BAD_REQUEST, "이미 지원한 모집글입니다."), - ALREADY_APPROVED("SA003", HttpStatus.BAD_REQUEST, "이미 승인된 신청입니다."), + ALREADY_APPLYING("SA003", HttpStatus.BAD_REQUEST, "신청 상태가 APPLYING이 아닙니다."), LEADER_CANNOT_APPLY("SA004", HttpStatus.BAD_REQUEST, "스터디장은 스터디를 신청할 수 없습니다."), - LEADER_ONLY_APPROVED("SA005", HttpStatus.BAD_REQUEST, "스터디장만 승인이 가능합니다."); + LEADER_ONLY_APPROVED("SA005", HttpStatus.BAD_REQUEST, "스터디장만 승인이 가능합니다."), + LEADER_ONLY_REJECTED("SA006", HttpStatus.BAD_REQUEST, "스터디장만 거절이 가능합니다."); private final String code; private final HttpStatus status; diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java b/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java index f41cff98..03e54435 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java @@ -60,16 +60,27 @@ public void approveApplication(Long applicationId, Long userId) { Study study = findValidStudy(post); validateOnlyLeaderCanApproved(study, userId); - validateAlreadyApproved(application); + validateStatusIsApplying(application); application.approve(); StudyMember studyMember = StudyMember.createMember(study, application.getUserId()); studyMemberRepository.save(studyMember); - study.increaseMemberCount(); } + @Transactional + public void rejectApplication(Long applicationId, Long userId) { + StudyApplication application = findValidApplication(applicationId); + RecruitmentPost post = findValidRecruimentPost(application.getRecruitmentPostId()); + Study study = findValidStudy(post); + + validateOnlyLeaderCanRejected(study, userId); + validateStatusIsApplying(application); + + application.reject(); + } + private Study findValidStudy(RecruitmentPost post) { Study study = studyRepository.findByIdAndActivatedTrue(post.getStudyId()) .orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND)); @@ -102,9 +113,9 @@ private void validateNotAlreadyApplied(ApplicationCreateRequest request) { } } - private static void validateAlreadyApproved(StudyApplication application) { - if (application.getStatus() == ApplicationStatus.APPROVED) { - throw new BusinessException(ALREADY_APPROVED); + private static void validateStatusIsApplying(StudyApplication application) { + if (application.getStatus() != ApplicationStatus.APPLYING) { + throw new BusinessException(ALREADY_APPLYING); } } @@ -114,4 +125,11 @@ private void validateOnlyLeaderCanApproved(Study study, Long userId) { throw new BusinessException(LEADER_ONLY_APPROVED); } } + + private void validateOnlyLeaderCanRejected(Study study, Long userId) { + boolean isLeader = studyMemberRepository.existsByStudyIdAndUserIdAndRole(study.getId(), userId, StudyMemberRole.LEADER); + if (!isLeader) { + throw new BusinessException(LEADER_ONLY_REJECTED); + } + } } From ce964f214c261655bb8ef0ac8c7070ef1370e6b0 Mon Sep 17 00:00:00 2001 From: pia01190 Date: Tue, 22 Jul 2025 15:14:39 +0900 Subject: [PATCH 07/18] =?UTF-8?q?[EA3-168]=20refactor:=20ApplicationContro?= =?UTF-8?q?ller=20RequestMapping=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyapplication/controller/ApplicationController.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java index c241173d..3c987631 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java @@ -12,19 +12,19 @@ import java.util.List; -@RequestMapping("/api/applications") +@RequestMapping("/api/studies") @RequiredArgsConstructor @RestController public class ApplicationController implements ApplicationSpecification { private final ApplicationService applicationService; - @GetMapping + @GetMapping("/{studyId}/applications") public ApiResponse> getMyStudyApplications(@AuthenticationPrincipal Principal userDetails) { return ApiResponse.success(applicationService.getMyStudyApplications(userDetails.getUserId())); } - @PostMapping + @PostMapping("/{studyId}/applications") public ApiResponse createApplication(@RequestBody @Valid ApplicationCreateRequest request) { Long id = applicationService.createApplication(request); return ApiResponse.success(id); From f59f2bf300532584920495318e19e3838900ad78 Mon Sep 17 00:00:00 2001 From: dbrkdgus00 Date: Tue, 22 Jul 2025 15:27:54 +0900 Subject: [PATCH 08/18] =?UTF-8?q?[EA3-111]=20refactor=20:=20dto=20?= =?UTF-8?q?=EB=B0=8F=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../requset/GroupChatMessageRequestDto.java | 20 ++++----- .../dto/requset/GroupChatSwaggerRequest.java | 7 --- .../response/GroupChatMessageResponseDto.java | 43 ++++++------------- .../groupchat/entity/GroupChatMessage.java | 19 +++----- .../GroupChatMessageRepository.java | 3 +- .../groupchat/service/GroupChatService.java | 43 +++---------------- 6 files changed, 36 insertions(+), 99 deletions(-) diff --git a/src/main/java/grep/neogul_coder/domain/groupchat/controller/dto/requset/GroupChatMessageRequestDto.java b/src/main/java/grep/neogul_coder/domain/groupchat/controller/dto/requset/GroupChatMessageRequestDto.java index a2bdabfc..0df3d39f 100644 --- a/src/main/java/grep/neogul_coder/domain/groupchat/controller/dto/requset/GroupChatMessageRequestDto.java +++ b/src/main/java/grep/neogul_coder/domain/groupchat/controller/dto/requset/GroupChatMessageRequestDto.java @@ -1,6 +1,9 @@ package grep.neogul_coder.domain.groupchat.controller.dto.requset; +import grep.neogul_coder.domain.groupchat.entity.GroupChatMessage; +import grep.neogul_coder.domain.groupchat.entity.GroupChatRoom; import io.swagger.v3.oas.annotations.Hidden; +import java.time.LocalDateTime; import lombok.Getter; @Hidden @@ -18,15 +21,12 @@ public GroupChatMessageRequestDto(Long roomId, Long senderId, String message) { this.message = message; } - public void setRoomId(Long roomId) { - this.roomId = roomId; - } - - public void setSenderId(Long senderId) { - this.senderId = senderId; - } - - public void setMessage(String message) { - this.message = message; + public GroupChatMessage toEntity(GroupChatRoom room, Long senderId) { + return new GroupChatMessage( + room, + senderId, + this.message, + LocalDateTime.now() + ); } } diff --git a/src/main/java/grep/neogul_coder/domain/groupchat/controller/dto/requset/GroupChatSwaggerRequest.java b/src/main/java/grep/neogul_coder/domain/groupchat/controller/dto/requset/GroupChatSwaggerRequest.java index bf5f1f1c..53fa2e13 100644 --- a/src/main/java/grep/neogul_coder/domain/groupchat/controller/dto/requset/GroupChatSwaggerRequest.java +++ b/src/main/java/grep/neogul_coder/domain/groupchat/controller/dto/requset/GroupChatSwaggerRequest.java @@ -15,11 +15,4 @@ public class GroupChatSwaggerRequest { @Schema(description = "보낼 메시지", example = "안녕하세요!") private String message; - - - public void setSenderId(Long senderId) { this.senderId = senderId; } - - public void setRoomId(Long roomId) { this.roomId = roomId; } - - public void setMessage(String message) { this.message = message; } } diff --git a/src/main/java/grep/neogul_coder/domain/groupchat/controller/dto/response/GroupChatMessageResponseDto.java b/src/main/java/grep/neogul_coder/domain/groupchat/controller/dto/response/GroupChatMessageResponseDto.java index 35e8a843..48c687cc 100644 --- a/src/main/java/grep/neogul_coder/domain/groupchat/controller/dto/response/GroupChatMessageResponseDto.java +++ b/src/main/java/grep/neogul_coder/domain/groupchat/controller/dto/response/GroupChatMessageResponseDto.java @@ -1,5 +1,7 @@ package grep.neogul_coder.domain.groupchat.controller.dto.response; +import grep.neogul_coder.domain.groupchat.entity.GroupChatMessage; +import grep.neogul_coder.domain.users.entity.User; import io.swagger.v3.oas.annotations.Hidden; import java.time.LocalDateTime; import lombok.Getter; @@ -16,10 +18,7 @@ public class GroupChatMessageResponseDto { private String message; // 메시지 내용 private LocalDateTime sentAt; // 보낸 시간 - public GroupChatMessageResponseDto() { - } - - public GroupChatMessageResponseDto(Long id, Long roomId, Long senderId, + private GroupChatMessageResponseDto(Long id, Long roomId, Long senderId, String senderNickname, String profileImageUrl, String message, LocalDateTime sentAt) { this.id = id; @@ -31,31 +30,15 @@ public GroupChatMessageResponseDto(Long id, Long roomId, Long senderId, this.sentAt = sentAt; } - public void setId(Long id) { - this.id = id; - } - - public void setRoomId(Long roomId) { - this.roomId = roomId; - } - - public void setSenderId(Long senderId) { - this.senderId = senderId; - } - - public void setSenderNickname(String senderNickname) { - this.senderNickname = senderNickname; - } - - public void setProfileImageUrl(String profileImageUrl) { - this.profileImageUrl = profileImageUrl; - } - - public void setMessage(String message) { - this.message = message; - } - - public void setSentAt(LocalDateTime sentAt) { - this.sentAt = sentAt; + public static GroupChatMessageResponseDto from(GroupChatMessage message, User sender) { + return new GroupChatMessageResponseDto( + message.getMessageId(), + message.getGroupChatRoom().getRoomId(), + sender.getId(), + sender.getNickname(), + sender.getProfileImageUrl(), + message.getMessage(), + message.getSentAt() + ); } } diff --git a/src/main/java/grep/neogul_coder/domain/groupchat/entity/GroupChatMessage.java b/src/main/java/grep/neogul_coder/domain/groupchat/entity/GroupChatMessage.java index 10ba941e..5f9e141c 100644 --- a/src/main/java/grep/neogul_coder/domain/groupchat/entity/GroupChatMessage.java +++ b/src/main/java/grep/neogul_coder/domain/groupchat/entity/GroupChatMessage.java @@ -27,23 +27,14 @@ public class GroupChatMessage extends BaseEntity { private LocalDateTime sentAt; - public void setMessageId(Long messageId) { - this.messageId = messageId; - } - - public void setGroupChatRoom(GroupChatRoom groupChatRoom) { - this.groupChatRoom = groupChatRoom; - } - - public void setUserId(Long userId) { + public GroupChatMessage(GroupChatRoom room, Long userId, String message, LocalDateTime sentAt) { + this.groupChatRoom = room; this.userId = userId; - } - - public void setMessage(String message) { this.message = message; + this.sentAt = sentAt; } - public void setSentAt(LocalDateTime sentAt) { - this.sentAt = sentAt; + protected GroupChatMessage() { + } } diff --git a/src/main/java/grep/neogul_coder/domain/groupchat/repository/GroupChatMessageRepository.java b/src/main/java/grep/neogul_coder/domain/groupchat/repository/GroupChatMessageRepository.java index c5437d47..515c3cb4 100644 --- a/src/main/java/grep/neogul_coder/domain/groupchat/repository/GroupChatMessageRepository.java +++ b/src/main/java/grep/neogul_coder/domain/groupchat/repository/GroupChatMessageRepository.java @@ -9,11 +9,10 @@ public interface GroupChatMessageRepository extends JpaRepository { - // 채팅방(roomId)에 속한 메시지를 전송 시간 내림차순으로 페이징 조회 @Query("SELECT m FROM GroupChatMessage m " + "WHERE m.groupChatRoom.roomId = :roomId " + "ORDER BY m.sentAt ASC") Page findMessagesByRoomIdAsc(@Param("roomId") Long roomId, Pageable pageable); -} +} \ No newline at end of file diff --git a/src/main/java/grep/neogul_coder/domain/groupchat/service/GroupChatService.java b/src/main/java/grep/neogul_coder/domain/groupchat/service/GroupChatService.java index ddce78be..381c5afd 100644 --- a/src/main/java/grep/neogul_coder/domain/groupchat/service/GroupChatService.java +++ b/src/main/java/grep/neogul_coder/domain/groupchat/service/GroupChatService.java @@ -10,6 +10,7 @@ import grep.neogul_coder.domain.users.entity.User; import grep.neogul_coder.domain.users.repository.UserRepository; import grep.neogul_coder.global.response.PageResponse; +import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -17,6 +18,7 @@ import org.springframework.stereotype.Service; import java.time.LocalDateTime; +@RequiredArgsConstructor @Service public class GroupChatService { @@ -25,17 +27,6 @@ public class GroupChatService { private final UserRepository userRepository; private final StudyMemberRepository studyMemberRepository; - // 생성자 주입을 통한 의존성 주입 - public GroupChatService(GroupChatMessageRepository messageRepository, - GroupChatRoomRepository roomRepository, - UserRepository userRepository, - StudyMemberRepository studyMemberRepository) { - this.messageRepository = messageRepository; - this.roomRepository = roomRepository; - this.userRepository = userRepository; - this.studyMemberRepository = studyMemberRepository; - } - public GroupChatMessageResponseDto saveMessage(GroupChatMessageRequestDto requestDto) { // 채팅방 존재 여부 확인 GroupChatRoom room = roomRepository.findById(requestDto.getRoomId()) @@ -51,26 +42,14 @@ public GroupChatMessageResponseDto saveMessage(GroupChatMessageRequestDto reques throw new IllegalArgumentException("해당 스터디에 참가한 사용자만 채팅할 수 있습니다."); } - // 메시지 생성 및 저장 - GroupChatMessage message = new GroupChatMessage(); - message.setGroupChatRoom(room); // 엔티티에 있는 필드명 맞게 사용 - message.setUserId(sender.getId()); // 연관관계 없이 userId만 저장 - message.setMessage(requestDto.getMessage()); - message.setSentAt(LocalDateTime.now()); + // 메시지 생성 + GroupChatMessage message = requestDto.toEntity(room, sender.getId()); // 메시지 저장 messageRepository.save(message); - // 저장된 메시지를 dto로 변환 - return new GroupChatMessageResponseDto( - message.getMessageId(), - message.getGroupChatRoom().getRoomId(), // ← roomId 필드가 필요하면 여기서 꺼내야 함 - sender.getId(), - sender.getNickname(), - sender.getProfileImageUrl(), - message.getMessage(), - message.getSentAt() - ); + // 응답 dto 생성 + return GroupChatMessageResponseDto.from(message, sender); } // 과거 채팅 메시지 페이징 조회 (무한 스크롤용) @@ -86,15 +65,7 @@ public PageResponse getMessages(Long roomId, int pa User sender = userRepository.findById(message.getUserId()) .orElseThrow(() -> new IllegalArgumentException("사용자가 존재하지 않습니다.")); - return new GroupChatMessageResponseDto( - message.getMessageId(), - message.getGroupChatRoom().getRoomId(), - sender.getId(), - sender.getNickname(), - sender.getProfileImageUrl(), - message.getMessage(), - message.getSentAt() - ); + return GroupChatMessageResponseDto.from(message, sender); }); // PageResponse로 감싸서 반환 From 36a27df8b2a45611fc5c2ee50c893e25a14f1791 Mon Sep 17 00:00:00 2001 From: pia01190 Date: Tue, 22 Jul 2025 15:32:11 +0900 Subject: [PATCH 09/18] =?UTF-8?q?[EA3-168]=20refactor:=20ApplicationCreate?= =?UTF-8?q?Request=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ApplicationController.java | 19 +++++++----- .../controller/ApplicationSpecification.java | 4 +-- .../dto/request/ApplicationCreateRequest.java | 17 +++-------- .../service/ApplicationService.java | 30 +++++++++---------- 4 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java index 3c987631..2f82ef1a 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java @@ -12,32 +12,35 @@ import java.util.List; -@RequestMapping("/api/studies") +@RequestMapping("/api/recruitment-posts") @RequiredArgsConstructor @RestController public class ApplicationController implements ApplicationSpecification { private final ApplicationService applicationService; - @GetMapping("/{studyId}/applications") - public ApiResponse> getMyStudyApplications(@AuthenticationPrincipal Principal userDetails) { + @GetMapping("/{recruitment-post-id}/applications") + public ApiResponse> getMyStudyApplications(@PathVariable("recruitment-post-id") Long recruitmentPostId, + @AuthenticationPrincipal Principal userDetails) { return ApiResponse.success(applicationService.getMyStudyApplications(userDetails.getUserId())); } - @PostMapping("/{studyId}/applications") - public ApiResponse createApplication(@RequestBody @Valid ApplicationCreateRequest request) { - Long id = applicationService.createApplication(request); + @PostMapping("/{recruitment-post-id}/applications") + public ApiResponse createApplication(@PathVariable("recruitment-post-id") Long recruitmentPostId, + @RequestBody @Valid ApplicationCreateRequest request, + @AuthenticationPrincipal Principal userDetails) { + Long id = applicationService.createApplication(recruitmentPostId, request, userDetails.getUserId()); return ApiResponse.success(id); } - @PostMapping("/{applicationId}/approve") + @PostMapping("/applications/{applicationId}/approve") public ApiResponse approveApplication(@PathVariable("applicationId") Long applicationId, @AuthenticationPrincipal Principal userDetails) { applicationService.approveApplication(applicationId, userDetails.getUserId()); return ApiResponse.noContent(); } - @PostMapping("/{applicationId}/reject") + @PostMapping("/applications/{applicationId}/reject") public ApiResponse rejectApplication(@PathVariable("applicationId") Long applicationId, @AuthenticationPrincipal Principal userDetails) { applicationService.rejectApplication(applicationId, userDetails.getUserId()); diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java index e2f17b4f..fbc6a9fb 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java @@ -13,10 +13,10 @@ public interface ApplicationSpecification { @Operation(summary = "내 스터디 신청 목록 조회", description = "내가 신청한 스터디의 목록을 조회합니다.") - ApiResponse> getMyStudyApplications(Principal userDetails); + ApiResponse> getMyStudyApplications(Long recruitmentPostId, Principal userDetails); @Operation(summary = "스터디 신청 생성", description = "스터디를 신청합니다.") - ApiResponse createApplication(ApplicationCreateRequest request); + ApiResponse createApplication(Long recruitmentPostId, ApplicationCreateRequest request, Principal userDetails); @Operation(summary = "스터디 신청 승인", description = "스터디장이 스터디 신청을 승인합니다.") ApiResponse approveApplication(Long applicationId, Principal userDetails); diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/request/ApplicationCreateRequest.java b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/request/ApplicationCreateRequest.java index fa678377..80af23c7 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/request/ApplicationCreateRequest.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/request/ApplicationCreateRequest.java @@ -12,13 +12,6 @@ @Getter public class ApplicationCreateRequest { - @Schema(description = "모집글 번호", example = "1") - private Long recruitmentPostId; - - @NotNull - @Schema(description = "유저 번호", example = "1") - private Long userId; - @NotBlank @Schema(description = "스터디 신청 지원 동기", example = "자바를 더 공부하고싶어 지원하였습니다.") private String applicationReason; @@ -27,16 +20,14 @@ private ApplicationCreateRequest() { } @Builder - private ApplicationCreateRequest(Long recruitmentPostId, Long userId, String applicationReason) { - this.recruitmentPostId = recruitmentPostId; - this.userId = userId; + private ApplicationCreateRequest(String applicationReason) { this.applicationReason = applicationReason; } - public StudyApplication toEntity() { + public StudyApplication toEntity(Long recruitmentPostId, Long userId) { return StudyApplication.builder() - .recruitmentPostId(this.recruitmentPostId) - .userId(this.userId) + .recruitmentPostId(recruitmentPostId) + .userId(userId) .applicationReason(this.applicationReason) .isRead(false) .status(ApplicationStatus.APPLYING) diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java b/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java index 03e54435..f9b9109e 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java @@ -41,13 +41,13 @@ public List getMyStudyApplications(Long userId) { } @Transactional - public Long createApplication(ApplicationCreateRequest request) { - RecruitmentPost recruitmentPost = findValidRecruimentPost(request.getRecruitmentPostId()); + public Long createApplication(Long recruitmentPostId, ApplicationCreateRequest request, Long userId) { + RecruitmentPost recruitmentPost = findValidRecruimentPost(recruitmentPostId); - validateNotLeaderApply(request, recruitmentPost); - validateNotAlreadyApplied(request); + validateNotLeaderApply(recruitmentPost, userId); + validateNotAlreadyApplied(recruitmentPostId, userId); - StudyApplication application = request.toEntity(); + StudyApplication application = request.toEntity(recruitmentPostId, userId); applicationRepository.save(application); return application.getId(); @@ -59,7 +59,7 @@ public void approveApplication(Long applicationId, Long userId) { RecruitmentPost post = findValidRecruimentPost(application.getRecruitmentPostId()); Study study = findValidStudy(post); - validateOnlyLeaderCanApproved(study, userId); + validateOnlyLeaderCanApprove(study, userId); validateStatusIsApplying(application); application.approve(); @@ -75,7 +75,7 @@ public void rejectApplication(Long applicationId, Long userId) { RecruitmentPost post = findValidRecruimentPost(application.getRecruitmentPostId()); Study study = findValidStudy(post); - validateOnlyLeaderCanRejected(study, userId); + validateOnlyLeaderCanReject(study, userId); validateStatusIsApplying(application); application.reject(); @@ -93,21 +93,21 @@ private StudyApplication findValidApplication(Long applicationId) { return application; } - private RecruitmentPost findValidRecruimentPost(Long applicationId) { - RecruitmentPost post = recruitmentPostRepository.findByIdAndActivatedTrue(applicationId) + private RecruitmentPost findValidRecruimentPost(Long recruitmentPostId) { + RecruitmentPost post = recruitmentPostRepository.findByIdAndActivatedTrue(recruitmentPostId) .orElseThrow(() -> new NotFoundException(NOT_FOUND)); return post; } - private void validateNotLeaderApply(ApplicationCreateRequest request, RecruitmentPost recruitmentPost) { - boolean isLeader = studyMemberRepository.existsByStudyIdAndUserIdAndRole(recruitmentPost.getStudyId(), request.getUserId(), StudyMemberRole.LEADER); + private void validateNotLeaderApply(RecruitmentPost recruitmentPost, Long userId) { + boolean isLeader = studyMemberRepository.existsByStudyIdAndUserIdAndRole(recruitmentPost.getStudyId(), userId, StudyMemberRole.LEADER); if (isLeader) { throw new BusinessException(LEADER_CANNOT_APPLY); } } - private void validateNotAlreadyApplied(ApplicationCreateRequest request) { - boolean alreadyApplied = applicationRepository.existsByRecruitmentPostIdAndUserId(request.getRecruitmentPostId(), request.getUserId()); + private void validateNotAlreadyApplied(Long recruitmentPostId, Long userId) { + boolean alreadyApplied = applicationRepository.existsByRecruitmentPostIdAndUserId(recruitmentPostId, userId); if (alreadyApplied) { throw new BusinessException(ALREADY_APPLICATION); } @@ -119,14 +119,14 @@ private static void validateStatusIsApplying(StudyApplication application) { } } - private void validateOnlyLeaderCanApproved(Study study, Long userId) { + private void validateOnlyLeaderCanApprove(Study study, Long userId) { boolean isLeader = studyMemberRepository.existsByStudyIdAndUserIdAndRole(study.getId(), userId, StudyMemberRole.LEADER); if (!isLeader) { throw new BusinessException(LEADER_ONLY_APPROVED); } } - private void validateOnlyLeaderCanRejected(Study study, Long userId) { + private void validateOnlyLeaderCanReject(Study study, Long userId) { boolean isLeader = studyMemberRepository.existsByStudyIdAndUserIdAndRole(study.getId(), userId, StudyMemberRole.LEADER); if (!isLeader) { throw new BusinessException(LEADER_ONLY_REJECTED); From f27c120dee623999ca1fc12283e174c7894849db Mon Sep 17 00:00:00 2001 From: pia01190 Date: Tue, 22 Jul 2025 15:33:18 +0900 Subject: [PATCH 10/18] =?UTF-8?q?[EA3-168]=20chore:=20import=EB=AC=B8=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dto/request/ApplicationCreateRequest.java | 2 -- .../repository/ApplicationQueryRepository.java | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/request/ApplicationCreateRequest.java b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/request/ApplicationCreateRequest.java index 80af23c7..15f28be0 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/request/ApplicationCreateRequest.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/request/ApplicationCreateRequest.java @@ -1,11 +1,9 @@ package grep.neogul_coder.domain.studyapplication.controller.dto.request; -import grep.neogul_coder.domain.recruitment.post.RecruitmentPost; import grep.neogul_coder.domain.studyapplication.ApplicationStatus; import grep.neogul_coder.domain.studyapplication.StudyApplication; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/repository/ApplicationQueryRepository.java b/src/main/java/grep/neogul_coder/domain/studyapplication/repository/ApplicationQueryRepository.java index 231cad1b..a35640e2 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/repository/ApplicationQueryRepository.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/repository/ApplicationQueryRepository.java @@ -1,7 +1,6 @@ package grep.neogul_coder.domain.studyapplication.repository; import com.querydsl.jpa.impl.JPAQueryFactory; -import grep.neogul_coder.domain.study.enums.StudyMemberRole; import grep.neogul_coder.domain.studyapplication.controller.dto.response.MyApplicationResponse; import grep.neogul_coder.domain.studyapplication.controller.dto.response.QMyApplicationResponse; import jakarta.persistence.EntityManager; @@ -12,7 +11,7 @@ import static grep.neogul_coder.domain.recruitment.post.QRecruitmentPost.recruitmentPost; import static grep.neogul_coder.domain.study.QStudy.study; import static grep.neogul_coder.domain.study.QStudyMember.studyMember; -import static grep.neogul_coder.domain.study.enums.StudyMemberRole.*; +import static grep.neogul_coder.domain.study.enums.StudyMemberRole.LEADER; import static grep.neogul_coder.domain.studyapplication.QStudyApplication.studyApplication; import static grep.neogul_coder.domain.users.entity.QUser.user; From b362131458f292f5f76885ee5b4b61e5460f6f07 Mon Sep 17 00:00:00 2001 From: pia01190 Date: Tue, 22 Jul 2025 16:14:07 +0900 Subject: [PATCH 11/18] =?UTF-8?q?[EA3-168]=20feature:=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EC=8B=A0=EC=B2=AD=20=EC=83=9D=EC=84=B1,=20?= =?UTF-8?q?=EA=B1=B0=EC=A0=88,=20=EC=8A=B9=EC=9D=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/code/ApplicationErrorCode.java | 2 +- .../repository/ApplicationRepository.java | 3 + .../service/ApplicationService.java | 2 +- .../service/ApplicationServiceTest.java | 196 ++++++++++++++++++ 4 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 src/test/java/grep/neogul_coder/domain/studyapplication/service/ApplicationServiceTest.java diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java b/src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java index 2ad44295..5e5f268e 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/exception/code/ApplicationErrorCode.java @@ -9,7 +9,7 @@ public enum ApplicationErrorCode implements ErrorCode { APPLICATION_NOT_FOUND("SA001",HttpStatus.NOT_FOUND,"신청서를 찾을 수 없습니다."), ALREADY_APPLICATION("SA002", HttpStatus.BAD_REQUEST, "이미 지원한 모집글입니다."), - ALREADY_APPLYING("SA003", HttpStatus.BAD_REQUEST, "신청 상태가 APPLYING이 아닙니다."), + APPLICATION_NOT_APPLYING("SA003", HttpStatus.BAD_REQUEST, "신청 상태가 APPLYING이 아닙니다."), LEADER_CANNOT_APPLY("SA004", HttpStatus.BAD_REQUEST, "스터디장은 스터디를 신청할 수 없습니다."), LEADER_ONLY_APPROVED("SA005", HttpStatus.BAD_REQUEST, "스터디장만 승인이 가능합니다."), diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/repository/ApplicationRepository.java b/src/main/java/grep/neogul_coder/domain/studyapplication/repository/ApplicationRepository.java index aefa42ff..ad214d2c 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/repository/ApplicationRepository.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/repository/ApplicationRepository.java @@ -4,9 +4,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface ApplicationRepository extends JpaRepository { List findByRecruitmentPostId(Long recruitmentPostId); boolean existsByRecruitmentPostIdAndUserId(Long recruitmentPostId, Long userId); + + Optional findByIdAndActivatedTrue(Long applicationId); } diff --git a/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java b/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java index f9b9109e..bfae0aaf 100644 --- a/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java +++ b/src/main/java/grep/neogul_coder/domain/studyapplication/service/ApplicationService.java @@ -115,7 +115,7 @@ private void validateNotAlreadyApplied(Long recruitmentPostId, Long userId) { private static void validateStatusIsApplying(StudyApplication application) { if (application.getStatus() != ApplicationStatus.APPLYING) { - throw new BusinessException(ALREADY_APPLYING); + throw new BusinessException(APPLICATION_NOT_APPLYING); } } diff --git a/src/test/java/grep/neogul_coder/domain/studyapplication/service/ApplicationServiceTest.java b/src/test/java/grep/neogul_coder/domain/studyapplication/service/ApplicationServiceTest.java new file mode 100644 index 00000000..f7a183b0 --- /dev/null +++ b/src/test/java/grep/neogul_coder/domain/studyapplication/service/ApplicationServiceTest.java @@ -0,0 +1,196 @@ +package grep.neogul_coder.domain.studyapplication.service; + +import grep.neogul_coder.domain.IntegrationTestSupport; +import grep.neogul_coder.domain.recruitment.post.RecruitmentPost; +import grep.neogul_coder.domain.recruitment.post.repository.RecruitmentPostRepository; +import grep.neogul_coder.domain.study.Study; +import grep.neogul_coder.domain.study.StudyMember; +import grep.neogul_coder.domain.study.repository.StudyMemberRepository; +import grep.neogul_coder.domain.study.repository.StudyRepository; +import grep.neogul_coder.domain.studyapplication.StudyApplication; +import grep.neogul_coder.domain.studyapplication.controller.dto.request.ApplicationCreateRequest; +import grep.neogul_coder.domain.studyapplication.repository.ApplicationRepository; +import grep.neogul_coder.domain.users.entity.User; +import grep.neogul_coder.domain.users.repository.UserRepository; +import grep.neogul_coder.global.exception.business.BusinessException; +import jakarta.persistence.EntityManager; +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 java.time.LocalDateTime; +import java.util.List; + +import static grep.neogul_coder.domain.study.enums.StudyMemberRole.LEADER; +import static grep.neogul_coder.domain.study.enums.StudyMemberRole.MEMBER; +import static grep.neogul_coder.domain.studyapplication.ApplicationStatus.*; +import static grep.neogul_coder.domain.studyapplication.exception.code.ApplicationErrorCode.APPLICATION_NOT_APPLYING; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ApplicationServiceTest extends IntegrationTestSupport { + + @Autowired + private EntityManager em; + + @Autowired + private UserRepository userRepository; + + @Autowired + private StudyRepository studyRepository; + + @Autowired + private StudyMemberRepository studyMemberRepository; + + @Autowired + private RecruitmentPostRepository recruitmentPostRepository; + + @Autowired + private ApplicationService applicationService; + + @Autowired + private ApplicationRepository applicationRepository; + + private Long userId1; + private Long userId2; + private Long studyId; + private Long recruitmentPostId; + + @BeforeEach + void init() { + User user1 = createUser("test1"); + User user2 = createUser("test2"); + userRepository.saveAll(List.of(user1, user2)); + userId1 = user1.getId(); + userId2 = user2.getId(); + + Study study = createStudy("스터디", LocalDateTime.parse("2025-07-25T20:20:20"), LocalDateTime.parse("2025-07-28T20:20:20")); + studyRepository.save(study); + studyId = study.getId(); + + StudyMember studyLeader = createStudyLeader(study, userId1); + StudyMember studyMember = createStudyMember(study, userId2); + studyMemberRepository.saveAll(List.of(studyLeader, studyMember)); + + RecruitmentPost recruitmentPost = createRecruitmentPost(studyId, userId1, "제목", "내용", 1); + recruitmentPostRepository.save(recruitmentPost); + recruitmentPostId = recruitmentPost.getId(); + } + + @DisplayName("신청서를 생성합니다.") + @Test + void createApplication() { + // given + ApplicationCreateRequest request = ApplicationCreateRequest.builder() + .applicationReason("자바를 더 공부하고 싶어 지원합니다.") + .build(); + + // when + Long id = applicationService.createApplication(recruitmentPostId, request, userId2); + em.flush(); + em.clear(); + + // then + StudyApplication application = applicationRepository.findByIdAndActivatedTrue(id).orElseThrow(); + assertThat(application.getApplicationReason()).isEqualTo("자바를 더 공부하고 싶어 지원합니다."); + } + + @DisplayName("스터디장이 신청서를 승인합니다.") + @Test + void approveApplication() { + // given + StudyApplication application = createApplication(recruitmentPostId, userId2); + applicationRepository.save(application); + Long id = application.getId(); + + // when + applicationService.approveApplication(id, userId1); + em.flush(); + em.clear(); + + // then + assertThat(application.getStatus()).isEqualTo(APPROVED); + } + + @DisplayName("스터디장이 신청서를 거절합니다.") + @Test + void rejectApplication() { + // given + StudyApplication application = createApplication(recruitmentPostId, userId2); + applicationRepository.save(application); + Long id = application.getId(); + + // when + applicationService.rejectApplication(id, userId1); + em.flush(); + em.clear(); + + // then + assertThat(application.getStatus()).isEqualTo(REJECTED); + } + + @DisplayName("스터디장이 이미 승인이나 거절한 경우 신청서 거절 시 예외가 발생합니다.") + @Test + void rejectApplicationFail() { + // given + StudyApplication application = createApplication(recruitmentPostId, userId2); + applicationRepository.save(application); + Long id = application.getId(); + applicationService.rejectApplication(id, userId1); + + // when then + assertThatThrownBy(() -> + applicationService.rejectApplication(id, userId1)) + .isInstanceOf(BusinessException.class).hasMessage(APPLICATION_NOT_APPLYING.getMessage()); + } + + private static User createUser(String nickname) { + return User.builder() + .nickname(nickname) + .build(); + } + + private static Study createStudy(String name, LocalDateTime startDate, LocalDateTime endDate) { + return Study.builder() + .name(name) + .startDate(startDate) + .endDate(endDate) + .build(); + } + + private StudyMember createStudyLeader(Study study, Long userId) { + return StudyMember.builder() + .study(study) + .userId(userId) + .role(LEADER) + .build(); + } + + private StudyMember createStudyMember(Study study, Long userId) { + return StudyMember.builder() + .study(study) + .userId(userId) + .role(MEMBER) + .build(); + } + + private RecruitmentPost createRecruitmentPost(Long studyId ,Long userId, String subject, String content, int count) { + return RecruitmentPost.builder() + .subject(subject) + .content(content) + .recruitmentCount(count) + .studyId(studyId) + .userId(userId) + .build(); + } + + private StudyApplication createApplication(Long recruitmentPostId, Long userId) { + return StudyApplication.builder() + .recruitmentPostId(recruitmentPostId) + .userId(userId) + .applicationReason("자바를 더 공부하고 싶어 지원합니다.") + .status(APPLYING) + .build(); + } +} \ No newline at end of file From 002fc800a2d6a66c5d87cc58c8ea0b778c2a1e4a Mon Sep 17 00:00:00 2001 From: pia01190 Date: Tue, 22 Jul 2025 16:28:48 +0900 Subject: [PATCH 12/18] =?UTF-8?q?[EA3-167]=20refactor:=20study=20finished?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dto/response/AdminStudyResponse.java | 6 +++--- src/main/java/grep/neogul_coder/domain/study/Study.java | 3 +++ .../study/controller/dto/response/StudyItemResponse.java | 9 +++------ .../domain/study/repository/StudyQueryRepository.java | 6 ++++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/main/java/grep/neogul_coder/domain/admin/controller/dto/response/AdminStudyResponse.java b/src/main/java/grep/neogul_coder/domain/admin/controller/dto/response/AdminStudyResponse.java index 87b9a821..35e7d2ba 100644 --- a/src/main/java/grep/neogul_coder/domain/admin/controller/dto/response/AdminStudyResponse.java +++ b/src/main/java/grep/neogul_coder/domain/admin/controller/dto/response/AdminStudyResponse.java @@ -22,7 +22,7 @@ public class AdminStudyResponse { private Category category; @Schema(description = "스터디 종료 여부", example = "false") - private boolean isFinished; + private boolean finished; @Schema(description = "활성화 여부", example = "true") private boolean activated; @@ -33,7 +33,7 @@ private AdminStudyResponse(Long id, String name, Category category, boolean isFi this.id = id; this.name = name; this.category = category; - this.isFinished = isFinished; + this.finished = isFinished; this.activated = activated; } @@ -42,7 +42,7 @@ public static AdminStudyResponse from(Study study) { .id(study.getId()) .name(study.getName()) .category(study.getCategory()) - .isFinished(study.getEndDate().toLocalDate().isBefore(LocalDate.now())) + .isFinished(study.isFinished()) .activated(study.getActivated()) .build(); } diff --git a/src/main/java/grep/neogul_coder/domain/study/Study.java b/src/main/java/grep/neogul_coder/domain/study/Study.java index fb69e252..311efd81 100644 --- a/src/main/java/grep/neogul_coder/domain/study/Study.java +++ b/src/main/java/grep/neogul_coder/domain/study/Study.java @@ -43,6 +43,8 @@ public class Study extends BaseEntity { private boolean extended; + private boolean finished; + protected Study() {} @Builder @@ -60,6 +62,7 @@ private Study(Long originStudyId, String name, Category category, int capacity, this.introduction = introduction; this.imageUrl = imageUrl; this.extended = false; + this.finished = false; } public void update(String name, Category category, int capacity, StudyType studyType, diff --git a/src/main/java/grep/neogul_coder/domain/study/controller/dto/response/StudyItemResponse.java b/src/main/java/grep/neogul_coder/domain/study/controller/dto/response/StudyItemResponse.java index 26184c49..2cc0ecc9 100644 --- a/src/main/java/grep/neogul_coder/domain/study/controller/dto/response/StudyItemResponse.java +++ b/src/main/java/grep/neogul_coder/domain/study/controller/dto/response/StudyItemResponse.java @@ -1,11 +1,9 @@ package grep.neogul_coder.domain.study.controller.dto.response; import com.querydsl.core.annotations.QueryProjection; -import grep.neogul_coder.domain.study.Study; import grep.neogul_coder.domain.study.enums.Category; import grep.neogul_coder.domain.study.enums.StudyType; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; import lombok.Getter; import java.time.LocalDate; @@ -48,13 +46,11 @@ public class StudyItemResponse { private StudyType studyType; @Schema(description = "종료 여부", example = "false") - public boolean isFinished() { - return endDate.toLocalDate().isBefore(LocalDate.now()); - } + private boolean finished; @QueryProjection public StudyItemResponse(Long studyId, String name, String leaderNickname, int capacity, int currentCount, LocalDateTime startDate, - LocalDateTime endDate, String imageUrl, String introduction, Category category, StudyType studyType) { + LocalDateTime endDate, String imageUrl, String introduction, Category category, StudyType studyType, boolean finished) { this.studyId = studyId; this.name = name; this.leaderNickname = leaderNickname; @@ -66,5 +62,6 @@ public StudyItemResponse(Long studyId, String name, String leaderNickname, int c this.introduction = introduction; this.category = category; this.studyType = studyType; + this.finished = finished; } } diff --git a/src/main/java/grep/neogul_coder/domain/study/repository/StudyQueryRepository.java b/src/main/java/grep/neogul_coder/domain/study/repository/StudyQueryRepository.java index e3894009..865fc19b 100644 --- a/src/main/java/grep/neogul_coder/domain/study/repository/StudyQueryRepository.java +++ b/src/main/java/grep/neogul_coder/domain/study/repository/StudyQueryRepository.java @@ -42,7 +42,8 @@ public Page findMyStudiesPaging(Pageable pageable, Long userI study.imageUrl, study.introduction, study.category, - study.studyType + study.studyType, + study.finished )) .from(studyMember) .join(user).on(user.id.eq(studyMember.userId)) @@ -78,7 +79,8 @@ public List findMyStudies(Long userId) { study.imageUrl, study.introduction, study.category, - study.studyType + study.studyType, + study.finished )) .from(studyMember) .join(user).on(user.id.eq(studyMember.userId)) From c2376fcaff8bd077e0aea0a607c43ddaab66ee95 Mon Sep 17 00:00:00 2001 From: pia01190 Date: Tue, 22 Jul 2025 16:33:09 +0900 Subject: [PATCH 13/18] =?UTF-8?q?[EA3-167]=20refactor:=20sql=20study=20ins?= =?UTF-8?q?ert=EB=AC=B8=20finished=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/data.sql | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 98c35ea1..0b78cb55 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -4,11 +4,11 @@ INSERT INTO member (email, password, nickname, profile_image_url, role, oauth_id INSERT INTO member (email, password, nickname, profile_image_url, role, oauth_id, oauth_provider) VALUES ('rryu@yuhanhoesa.com', '$2a$10$WZzvlwlN6FVtQvGUXw2CIeNQvT5fPfA4qN99NisD2GOyCeuC4W0t2','NPy2EpXd$6', 'https://placekitten.com/842/456', 'ROLE_USER', NULL, NULL); INSERT INTO member (email, password, nickname, profile_image_url, role, oauth_id, oauth_provider) VALUES ('yeonghoseo@nate.com', '$2a$10$WZzvlwlN6FVtQvGUXw2CIeNQvT5fPfA4qN99NisD2GOyCeuC4W0t2', '$c3VDMSkF)','https://placekitten.com/812/623', 'ROLE_ADMIN', NULL, NULL); -INSERT INTO study (origin_study_id, name, category, capacity, current_count, study_type, location, start_date, end_date, introduction, image_url, extended, activated) VALUES (NULL, '자바 스터디', 'IT', 10, 1, 'ONLINE', NULL, '2025-08-01', '2025-12-31', '자바 스터디에 오신 것을 환영합니다.', 'https://example.com/image.jpg', FALSE, TRUE); -INSERT INTO study (origin_study_id, name, category, capacity, current_count, study_type, location, start_date, end_date, introduction, image_url, extended, activated) VALUES (NULL, '파이썬 스터디', 'IT', 8, 1, 'OFFLINE', '대구', '2025-09-01', '2026-01-31', '파이썬 기초부터 심화까지 학습합니다.', 'https://example.com/python.jpg', FALSE, TRUE); -INSERT INTO study (origin_study_id, name, category, capacity, current_count, study_type, location, start_date, end_date, introduction, image_url, extended, activated) VALUES (NULL, '디자인 스터디', 'DESIGN', 6, 1, 'HYBRID', '서울', '2025-07-15', '2025-10-15', 'UI/UX 디자인 실습 중심 스터디입니다.', 'https://example.com/design.jpg', FALSE, TRUE); -INSERT INTO study (origin_study_id, name, category, capacity, current_count, study_type, location, start_date, end_date, introduction, image_url, extended, activated) VALUES (NULL, '7급 공무원 스터디', 'EXAM', 12, 1, 'ONLINE', NULL, '2025-08-10', '2025-12-20', '7급 공무원 대비 스터디입니다.', 'https://example.com/exam.jpg', FALSE, TRUE); -INSERT INTO study (origin_study_id, name, category, capacity, current_count, study_type, location, start_date, end_date, introduction, image_url, extended, activated) VALUES (NULL, '토익 스터디', 'LANGUAGE', 9, 1, 'OFFLINE', '광주', '2025-09-05', '2026-02-28', '토익 스터디입니다.', 'https://example.com/datascience.jpg', FALSE, TRUE); +INSERT INTO study (origin_study_id, name, category, capacity, current_count, study_type, location, start_date, end_date, introduction, image_url, extended, activated, finished) VALUES (NULL, '자바 스터디', 'IT', 10, 1, 'ONLINE', NULL, '2025-08-01', '2025-12-31', '자바 스터디에 오신 것을 환영합니다.', 'https://example.com/image.jpg', FALSE, TRUE, FALSE); +INSERT INTO study (origin_study_id, name, category, capacity, current_count, study_type, location, start_date, end_date, introduction, image_url, extended, activated, finished) VALUES (NULL, '파이썬 스터디', 'IT', 8, 1, 'OFFLINE', '대구', '2025-09-01', '2026-01-31', '파이썬 기초부터 심화까지 학습합니다.', 'https://example.com/python.jpg', FALSE, TRUE, FALSE); +INSERT INTO study (origin_study_id, name, category, capacity, current_count, study_type, location, start_date, end_date, introduction, image_url, extended, activated, finished) VALUES (NULL, '디자인 스터디', 'DESIGN', 6, 1, 'HYBRID', '서울', '2025-07-15', '2025-10-15', 'UI/UX 디자인 실습 중심 스터디입니다.', 'https://example.com/design.jpg', FALSE, TRUE, FALSE); +INSERT INTO study (origin_study_id, name, category, capacity, current_count, study_type, location, start_date, end_date, introduction, image_url, extended, activated, finished) VALUES (NULL, '7급 공무원 스터디', 'EXAM', 12, 1, 'ONLINE', NULL, '2025-08-10', '2025-12-20', '7급 공무원 대비 스터디입니다.', 'https://example.com/exam.jpg', FALSE, TRUE, FALSE); +INSERT INTO study (origin_study_id, name, category, capacity, current_count, study_type, location, start_date, end_date, introduction, image_url, extended, activated, finished) VALUES (NULL, '토익 스터디', 'LANGUAGE', 9, 1, 'OFFLINE', '광주', '2025-09-05', '2026-02-28', '토익 스터디입니다.', 'https://example.com/datascience.jpg', FALSE, TRUE, FALSE); INSERT INTO study_post (study_id, user_id, title, category, content) VALUES (5, 3, '자바 스터디 1주차 공지.', 'NOTICE', '1주차 스터디 내용은 가위바위보 게임 만들기 입니다. 모두 각자 만드시고 설명 하는 시간을 가지겠습니다.'); INSERT INTO study_post (study_id, user_id, title, category, content) VALUES (4, 4, '익명 클래스 자료 공유', 'FREE', '동물 이라는 인터페이스가 있을때 구현체는 강아지, 고양이 등이 있습니다. 구현을 하면 여러 구현 클래스가 필요합니다 이를 줄이기 위해 익명클래스를 사용할 수 있습니다.'); From 23b9161349833d88c652d402706061e504125e7b Mon Sep 17 00:00:00 2001 From: pia01190 Date: Tue, 22 Jul 2025 17:08:51 +0900 Subject: [PATCH 14/18] =?UTF-8?q?[EA3-167]=20feature:=20=EC=A2=85=EB=A3=8C?= =?UTF-8?q?=207=EC=9D=BC=20=EC=A0=84=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=B0=BE=EB=8A=94=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/repository/StudyRepository.java | 4 +++ .../study/scheduler/StudyScheduler.java | 21 +++++++++++++++ .../study/service/StudySchedulerService.java | 27 +++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 src/main/java/grep/neogul_coder/domain/study/scheduler/StudyScheduler.java create mode 100644 src/main/java/grep/neogul_coder/domain/study/service/StudySchedulerService.java diff --git a/src/main/java/grep/neogul_coder/domain/study/repository/StudyRepository.java b/src/main/java/grep/neogul_coder/domain/study/repository/StudyRepository.java index 7dc60f71..6115c898 100644 --- a/src/main/java/grep/neogul_coder/domain/study/repository/StudyRepository.java +++ b/src/main/java/grep/neogul_coder/domain/study/repository/StudyRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.time.LocalDateTime; import java.util.Optional; import java.util.List; @@ -19,4 +20,7 @@ public interface StudyRepository extends JpaRepository { @Modifying(clearAutomatically = true) @Query("update Study s set s.activated = false where s.id = :studyId") void deactivateByStudyId(@Param("studyId") Long studyId); + + @Query("select s from Study s where s.endDate >= :endDateStart and s.endDate < :endDateEnd and s.finished = false and s.activated = true") + List findStudiesEndingIn7Days(@Param("endDateStart") LocalDateTime endDateStart, @Param("endDateEnd") LocalDateTime endDateEnd); } diff --git a/src/main/java/grep/neogul_coder/domain/study/scheduler/StudyScheduler.java b/src/main/java/grep/neogul_coder/domain/study/scheduler/StudyScheduler.java new file mode 100644 index 00000000..7fce6532 --- /dev/null +++ b/src/main/java/grep/neogul_coder/domain/study/scheduler/StudyScheduler.java @@ -0,0 +1,21 @@ +package grep.neogul_coder.domain.study.scheduler; + +import grep.neogul_coder.domain.study.Study; +import grep.neogul_coder.domain.study.service.StudySchedulerService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class StudyScheduler { + + private final StudySchedulerService studySchedulerService; + + @Scheduled(cron = "0 0 0 * * *") + public void processEndingStudies() { + List studiesEndingIn7Days = studySchedulerService.findStudiesEndingIn7Days(); + } +} diff --git a/src/main/java/grep/neogul_coder/domain/study/service/StudySchedulerService.java b/src/main/java/grep/neogul_coder/domain/study/service/StudySchedulerService.java new file mode 100644 index 00000000..f0d85341 --- /dev/null +++ b/src/main/java/grep/neogul_coder/domain/study/service/StudySchedulerService.java @@ -0,0 +1,27 @@ +package grep.neogul_coder.domain.study.service; + +import grep.neogul_coder.domain.study.Study; +import grep.neogul_coder.domain.study.repository.StudyRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class StudySchedulerService { + + private final StudyRepository studyRepository; + + public List findStudiesEndingIn7Days() { + LocalDate targetEndDate = LocalDate.now().plusDays(7); + LocalDateTime endDateStart = targetEndDate.atStartOfDay(); + LocalDateTime endDateEnd = targetEndDate.plusDays(1).atStartOfDay(); + + return studyRepository.findStudiesEndingIn7Days(endDateStart, endDateEnd); + } +} From 85ce98f61235ef9c2dd058fc64f13fe9e72ce040 Mon Sep 17 00:00:00 2001 From: pia01190 Date: Tue, 22 Jul 2025 17:49:55 +0900 Subject: [PATCH 15/18] =?UTF-8?q?[EA3-167]=20feature:=20=EC=A2=85=EB=A3=8C?= =?UTF-8?q?=20=EB=82=A0=EC=A7=9C=20=EC=A7=80=EB=82=9C=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EC=A2=85=EB=A3=8C=20=EC=83=81=ED=83=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/grep/neogul_coder/domain/study/Study.java | 4 ++++ .../domain/study/repository/StudyRepository.java | 3 +++ .../domain/study/scheduler/StudyScheduler.java | 2 ++ .../domain/study/service/StudySchedulerService.java | 10 ++++++++++ 4 files changed, 19 insertions(+) diff --git a/src/main/java/grep/neogul_coder/domain/study/Study.java b/src/main/java/grep/neogul_coder/domain/study/Study.java index 311efd81..b42d51a1 100644 --- a/src/main/java/grep/neogul_coder/domain/study/Study.java +++ b/src/main/java/grep/neogul_coder/domain/study/Study.java @@ -100,4 +100,8 @@ public boolean alreadyExtended() { public void extend() { this.extended = true; } + + public void finish() { + this.finished = true; + } } diff --git a/src/main/java/grep/neogul_coder/domain/study/repository/StudyRepository.java b/src/main/java/grep/neogul_coder/domain/study/repository/StudyRepository.java index 6115c898..ac39e29c 100644 --- a/src/main/java/grep/neogul_coder/domain/study/repository/StudyRepository.java +++ b/src/main/java/grep/neogul_coder/domain/study/repository/StudyRepository.java @@ -23,4 +23,7 @@ public interface StudyRepository extends JpaRepository { @Query("select s from Study s where s.endDate >= :endDateStart and s.endDate < :endDateEnd and s.finished = false and s.activated = true") List findStudiesEndingIn7Days(@Param("endDateStart") LocalDateTime endDateStart, @Param("endDateEnd") LocalDateTime endDateEnd); + + @Query("select s from Study s where s.endDate < :now and s.finished = false and s.activated = true") + List findStudiesToBeFinished(@Param("now") LocalDateTime now); } diff --git a/src/main/java/grep/neogul_coder/domain/study/scheduler/StudyScheduler.java b/src/main/java/grep/neogul_coder/domain/study/scheduler/StudyScheduler.java index 7fce6532..c814a455 100644 --- a/src/main/java/grep/neogul_coder/domain/study/scheduler/StudyScheduler.java +++ b/src/main/java/grep/neogul_coder/domain/study/scheduler/StudyScheduler.java @@ -17,5 +17,7 @@ public class StudyScheduler { @Scheduled(cron = "0 0 0 * * *") public void processEndingStudies() { List studiesEndingIn7Days = studySchedulerService.findStudiesEndingIn7Days(); + + studySchedulerService.finalizeStudies(); } } diff --git a/src/main/java/grep/neogul_coder/domain/study/service/StudySchedulerService.java b/src/main/java/grep/neogul_coder/domain/study/service/StudySchedulerService.java index f0d85341..6128638d 100644 --- a/src/main/java/grep/neogul_coder/domain/study/service/StudySchedulerService.java +++ b/src/main/java/grep/neogul_coder/domain/study/service/StudySchedulerService.java @@ -24,4 +24,14 @@ public List findStudiesEndingIn7Days() { return studyRepository.findStudiesEndingIn7Days(endDateStart, endDateEnd); } + + @Transactional + public void finalizeStudies() { + LocalDateTime now = LocalDateTime.now(); + List studiesToBeFinished = studyRepository.findStudiesToBeFinished(now); + + for (Study study : studiesToBeFinished) { + study.finish(); + } + } } From 87078b98b979ac2743a5f3237632f5f1a9017c0f Mon Sep 17 00:00:00 2001 From: pia01190 Date: Tue, 22 Jul 2025 17:59:34 +0900 Subject: [PATCH 16/18] =?UTF-8?q?[EA3-167]=20chore:=20import=EB=AC=B8=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/controller/dto/response/StudyItemResponse.java | 1 - .../neogul_coder/domain/study/repository/StudyRepository.java | 2 +- .../domain/study/service/StudyManagementService.java | 3 ++- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/grep/neogul_coder/domain/study/controller/dto/response/StudyItemResponse.java b/src/main/java/grep/neogul_coder/domain/study/controller/dto/response/StudyItemResponse.java index 2cc0ecc9..b899e602 100644 --- a/src/main/java/grep/neogul_coder/domain/study/controller/dto/response/StudyItemResponse.java +++ b/src/main/java/grep/neogul_coder/domain/study/controller/dto/response/StudyItemResponse.java @@ -6,7 +6,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; -import java.time.LocalDate; import java.time.LocalDateTime; @Getter diff --git a/src/main/java/grep/neogul_coder/domain/study/repository/StudyRepository.java b/src/main/java/grep/neogul_coder/domain/study/repository/StudyRepository.java index ac39e29c..c259e093 100644 --- a/src/main/java/grep/neogul_coder/domain/study/repository/StudyRepository.java +++ b/src/main/java/grep/neogul_coder/domain/study/repository/StudyRepository.java @@ -7,8 +7,8 @@ import org.springframework.data.repository.query.Param; import java.time.LocalDateTime; -import java.util.Optional; import java.util.List; +import java.util.Optional; public interface StudyRepository extends JpaRepository { List findByIdIn(List studyIds); diff --git a/src/main/java/grep/neogul_coder/domain/study/service/StudyManagementService.java b/src/main/java/grep/neogul_coder/domain/study/service/StudyManagementService.java index 209dd301..e97a4c23 100644 --- a/src/main/java/grep/neogul_coder/domain/study/service/StudyManagementService.java +++ b/src/main/java/grep/neogul_coder/domain/study/service/StudyManagementService.java @@ -19,7 +19,8 @@ import java.util.List; import java.util.Random; -import static grep.neogul_coder.domain.study.enums.StudyMemberRole.*; +import static grep.neogul_coder.domain.study.enums.StudyMemberRole.LEADER; +import static grep.neogul_coder.domain.study.enums.StudyMemberRole.MEMBER; import static grep.neogul_coder.domain.study.exception.code.StudyErrorCode.*; @Transactional(readOnly = true) From fe48dd54c9662bc2ce76bf549bead4961eeaf1d6 Mon Sep 17 00:00:00 2001 From: hyeunS-P Date: Tue, 22 Jul 2025 19:50:26 +0900 Subject: [PATCH 17/18] =?UTF-8?q?[EA3-173]=20feature:=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=8B=9C=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B3=BC=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../users/controller/UserController.java | 21 ++++++ .../users/controller/UserSpecification.java | 17 +++++ .../domain/users/entity/User.java | 22 ++++-- .../exception/NotVerifiedEmailException.java | 11 +++ .../users/exception/advice/UserAdvice.java | 8 ++ .../users/exception/code/UserErrorCode.java | 3 +- .../service/EmailVerificationService.java | 74 +++++++++++++++++++ .../domain/users/service/UserService.java | 9 ++- .../global/auth/service/AuthService.java | 27 +++++-- .../global/config/MailConfig.java | 45 +++++++++++ .../config/security/SecurityConfig.java | 4 +- 12 files changed, 227 insertions(+), 17 deletions(-) create mode 100644 src/main/java/grep/neogul_coder/domain/users/exception/NotVerifiedEmailException.java create mode 100644 src/main/java/grep/neogul_coder/domain/users/service/EmailVerificationService.java create mode 100644 src/main/java/grep/neogul_coder/global/config/MailConfig.java diff --git a/build.gradle b/build.gradle index 02b3789e..0ffbf1f5 100644 --- a/build.gradle +++ b/build.gradle @@ -91,6 +91,9 @@ dependencies { // WebSocket + STOMP 통신용 implementation 'org.springframework.boot:spring-boot-starter-websocket' + + // 이메일 전송 의존성 + implementation 'org.springframework.boot:spring-boot-starter-mail' } tasks.named('test') { diff --git a/src/main/java/grep/neogul_coder/domain/users/controller/UserController.java b/src/main/java/grep/neogul_coder/domain/users/controller/UserController.java index 55c6b3c9..0a5fd2c1 100644 --- a/src/main/java/grep/neogul_coder/domain/users/controller/UserController.java +++ b/src/main/java/grep/neogul_coder/domain/users/controller/UserController.java @@ -4,6 +4,7 @@ import grep.neogul_coder.domain.users.controller.dto.request.SignUpRequest; import grep.neogul_coder.domain.users.controller.dto.request.UpdatePasswordRequest; import grep.neogul_coder.domain.users.controller.dto.response.UserResponse; +import grep.neogul_coder.domain.users.service.EmailVerificationService; import grep.neogul_coder.domain.users.service.UserService; import grep.neogul_coder.global.auth.Principal; import grep.neogul_coder.global.response.ApiResponse; @@ -20,6 +21,7 @@ import org.springframework.web.bind.annotation.PutMapping; 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.RequestPart; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @@ -31,6 +33,7 @@ public class UserController implements UserSpecification { private final UserService usersService; + private final EmailVerificationService verificationService; @GetMapping("/me") public ApiResponse get(@AuthenticationPrincipal Principal principal) { @@ -75,4 +78,22 @@ public ApiResponse signUp(@Valid @RequestBody SignUpRequest request) { usersService.signUp(request); return ApiResponse.noContent(); } + + @PostMapping("/mail/send") + public ApiResponse sendCode(@RequestParam String email) { + verificationService.sendVerificationEmail(email); + return ApiResponse.noContent(); + } + + @PostMapping("/mail/verify") + public ApiResponse verifyCode( + @RequestParam String email, + @RequestParam String code + ) { + boolean result = verificationService.verifyCode(email, code); + return result ? + ApiResponse.noContent() : + ApiResponse.badRequest(); + } + } \ No newline at end of file diff --git a/src/main/java/grep/neogul_coder/domain/users/controller/UserSpecification.java b/src/main/java/grep/neogul_coder/domain/users/controller/UserSpecification.java index a9ea9c9e..e68c9425 100644 --- a/src/main/java/grep/neogul_coder/domain/users/controller/UserSpecification.java +++ b/src/main/java/grep/neogul_coder/domain/users/controller/UserSpecification.java @@ -7,10 +7,12 @@ import grep.neogul_coder.global.auth.Principal; import grep.neogul_coder.global.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.tags.Tag; import java.io.IOException; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; @@ -36,4 +38,19 @@ ApiResponse updatePassword(@AuthenticationPrincipal Principal principal, @Operation(summary = "회원 상태 삭제로 변경", description = "회원 상태를 삭제로 변경합니다.") ApiResponse delete(@AuthenticationPrincipal Principal principal, @RequestBody PasswordRequest request); + + @Operation(summary = "이메일 인증 코드 발송", description = "입력한 이메일 주소로 인증 코드를 발송합니다.") + ApiResponse sendCode( + @Parameter(description = "인증 코드를 보낼 이메일 주소", required = true, example = "user@example.com") + @RequestParam String email + ); + + @Operation(summary = "이메일 인증 코드 검증", description = "사용자가 입력한 인증 코드가 올바른지 검증합니다.") + ApiResponse verifyCode( + @Parameter(description = "인증 요청한 이메일 주소", required = true, example = "user@example.com") + @RequestParam String email, + + @Parameter(description = "사용자가 입력한 인증 코드", required = true, example = "123456") + @RequestParam String code + ); } \ No newline at end of file diff --git a/src/main/java/grep/neogul_coder/domain/users/entity/User.java b/src/main/java/grep/neogul_coder/domain/users/entity/User.java index d46f8321..49c19829 100644 --- a/src/main/java/grep/neogul_coder/domain/users/entity/User.java +++ b/src/main/java/grep/neogul_coder/domain/users/entity/User.java @@ -2,7 +2,13 @@ import grep.neogul_coder.global.auth.code.Role; import grep.neogul_coder.global.entity.BaseEntity; -import jakarta.persistence.*; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import jakarta.validation.constraints.Email; import lombok.Builder; import lombok.Getter; @@ -34,11 +40,12 @@ public class User extends BaseEntity { public static User UserInit(String email, String password, String nickname) { return User.builder() - .email(email) - .password(password) - .nickname(nickname) - .role(Role.ROLE_USER) - .build(); + .email(email) + .password(password) + .nickname(nickname) + .role(Role.ROLE_USER) + .activated(true) + .build(); } public void updateProfile(String nickname, String profileImageUrl) { @@ -58,7 +65,7 @@ public void delete() { @Builder private User(Long id, String oauthId, String oauthProvider, String email, String password, - String nickname, String profileImageUrl, Role role) { + String nickname, String profileImageUrl, Boolean activated, Role role) { this.id = id; this.oauthId = oauthId; this.oauthProvider = oauthProvider; @@ -66,6 +73,7 @@ private User(Long id, String oauthId, String oauthProvider, String email, String this.password = password; this.nickname = nickname; this.profileImageUrl = profileImageUrl; + this.activated = activated; this.role = role; } diff --git a/src/main/java/grep/neogul_coder/domain/users/exception/NotVerifiedEmailException.java b/src/main/java/grep/neogul_coder/domain/users/exception/NotVerifiedEmailException.java new file mode 100644 index 00000000..1c9d725d --- /dev/null +++ b/src/main/java/grep/neogul_coder/domain/users/exception/NotVerifiedEmailException.java @@ -0,0 +1,11 @@ +package grep.neogul_coder.domain.users.exception; + +import grep.neogul_coder.global.exception.business.BusinessException; +import grep.neogul_coder.global.response.code.ErrorCode; + +public class NotVerifiedEmailException extends BusinessException { + + public NotVerifiedEmailException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/grep/neogul_coder/domain/users/exception/advice/UserAdvice.java b/src/main/java/grep/neogul_coder/domain/users/exception/advice/UserAdvice.java index f100594e..d9692afb 100644 --- a/src/main/java/grep/neogul_coder/domain/users/exception/advice/UserAdvice.java +++ b/src/main/java/grep/neogul_coder/domain/users/exception/advice/UserAdvice.java @@ -2,6 +2,7 @@ import grep.neogul_coder.domain.users.exception.EmailDuplicationException; import grep.neogul_coder.domain.users.exception.NicknameDuplicatedException; +import grep.neogul_coder.domain.users.exception.NotVerifiedEmailException; import grep.neogul_coder.domain.users.exception.PasswordNotMatchException; import grep.neogul_coder.domain.users.exception.PasswordUncheckException; import grep.neogul_coder.domain.users.exception.UserNotFoundException; @@ -50,4 +51,11 @@ public ResponseEntity> nicknameDuplicationException(NicknameDu .body(ApiResponse.errorWithoutData(UserErrorCode.IS_DUPLICATED_NICKNAME)); } + @ ExceptionHandler(NotVerifiedEmailException.class) + public ResponseEntity> notVerifiedEmailException(NotVerifiedEmailException ex) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(ApiResponse.errorWithoutData(UserErrorCode.NOT_VERIFIED_EMAIL)); + } + } \ No newline at end of file diff --git a/src/main/java/grep/neogul_coder/domain/users/exception/code/UserErrorCode.java b/src/main/java/grep/neogul_coder/domain/users/exception/code/UserErrorCode.java index e868689d..5a2899ae 100644 --- a/src/main/java/grep/neogul_coder/domain/users/exception/code/UserErrorCode.java +++ b/src/main/java/grep/neogul_coder/domain/users/exception/code/UserErrorCode.java @@ -12,7 +12,8 @@ public enum UserErrorCode implements ErrorCode { PASSWORD_UNCHECKED("U003", HttpStatus.BAD_REQUEST, "비밀번호와 비밀번호 확인이 다릅니다"), IS_DUPLICATED_MALI("U004", HttpStatus.BAD_REQUEST,"이미 존재하는 이메일입니다."), IS_DUPLICATED_NICKNAME("U005", HttpStatus.BAD_REQUEST,"이미 존재하는 닉네임입니다."), - UNACTIVATED_USER("U006", HttpStatus.BAD_REQUEST,"탈퇴된 회원입니다."); + UNACTIVATED_USER("U006", HttpStatus.BAD_REQUEST,"탈퇴된 회원입니다."), + NOT_VERIFIED_EMAIL("U007", HttpStatus.BAD_REQUEST, "메일 인증이 되지 않은 이메일입니다."); private final String code; diff --git a/src/main/java/grep/neogul_coder/domain/users/service/EmailVerificationService.java b/src/main/java/grep/neogul_coder/domain/users/service/EmailVerificationService.java new file mode 100644 index 00000000..309a69d5 --- /dev/null +++ b/src/main/java/grep/neogul_coder/domain/users/service/EmailVerificationService.java @@ -0,0 +1,74 @@ +package grep.neogul_coder.domain.users.service; + +import java.time.Duration; +import java.util.Random; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class EmailVerificationService { + + private final JavaMailSender mailSender; + private final RedisTemplate redisTemplate; + + private static final long CODE_TTL_SECONDS = 300; + + public void sendVerificationEmail(String email) { + String code = generateRandomCode(); + + sendEmail(email,code); + + redisTemplate.opsForValue().set(getRedisKey(email), code, Duration.ofSeconds(CODE_TTL_SECONDS)); + } + + public boolean verifyCode(String email, String inputCode) { + String redisKey = getRedisKey(email); + Object storedCode = redisTemplate.opsForValue().get(getRedisKey(email)); + + + if (isValidCode(storedCode,inputCode)) { + redisTemplate.delete(redisKey); + + redisTemplate.opsForValue().set(getVerifiedKey(email), "true", Duration.ofMinutes(10)); + return true; + } + return false; + } + + public boolean isNotEmailVerified(String email) { + Object value = redisTemplate.opsForValue().get(getVerifiedKey(email)); + return !"true".equals(value); + } + + public void clearVerifiedStatus(String email) { + redisTemplate.delete(getVerifiedKey(email)); + } + + private void sendEmail(String to, String code) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo(to); + message.setSubject("[wibby] 이메일 인증 코드"); + message.setText("인증 코드: " + code + "\n5분 안에 입력해주세요."); + mailSender.send(message); + } + + private String generateRandomCode() { + return String.format("%06d", new Random().nextInt(1000000)); + } + + private String getRedisKey(String email) { + return "email_verification:" + email; + } + + private String getVerifiedKey(String email) { + return "email_verified:" + email; + } + + private boolean isValidCode(Object storedCode, String inputCode) { + return storedCode != null && storedCode.equals(inputCode); + } +} diff --git a/src/main/java/grep/neogul_coder/domain/users/service/UserService.java b/src/main/java/grep/neogul_coder/domain/users/service/UserService.java index 6b2e319f..794bddcd 100644 --- a/src/main/java/grep/neogul_coder/domain/users/service/UserService.java +++ b/src/main/java/grep/neogul_coder/domain/users/service/UserService.java @@ -13,6 +13,7 @@ import grep.neogul_coder.domain.users.entity.User; import grep.neogul_coder.domain.users.exception.EmailDuplicationException; import grep.neogul_coder.domain.users.exception.NicknameDuplicatedException; +import grep.neogul_coder.domain.users.exception.NotVerifiedEmailException; import grep.neogul_coder.domain.users.exception.PasswordNotMatchException; import grep.neogul_coder.domain.users.exception.UserNotFoundException; import grep.neogul_coder.domain.users.exception.code.UserErrorCode; @@ -44,6 +45,7 @@ public class UserService { private final LinkService linkService; private final StudyManagementService studyManagementService; private final BuddyEnergyService buddyEnergyService; + private final EmailVerificationService verificationService; @Autowired(required = false) private GcpFileUploader gcpFileUploader; @@ -69,6 +71,10 @@ public void signUp(SignUpRequest request) { duplicationCheck(request.getEmail(), request.getNickname()); + if (verificationService.isNotEmailVerified(request.getEmail())) { + throw new NotVerifiedEmailException(UserErrorCode.NOT_VERIFIED_EMAIL); + } + if (isNotMatchPasswordCheck(request.getPassword(), request.getPasswordCheck())) { throw new PasswordNotMatchException(UserErrorCode.PASSWORD_MISMATCH); } @@ -78,8 +84,9 @@ public void signUp(SignUpRequest request) { User.UserInit(request.getEmail(), encodedPassword, request.getNickname())); User user = findUser(request.getEmail()); - initializeUserData(user.getId()); + + verificationService.clearVerifiedStatus(request.getEmail()); } @Transactional diff --git a/src/main/java/grep/neogul_coder/global/auth/service/AuthService.java b/src/main/java/grep/neogul_coder/global/auth/service/AuthService.java index a13b3510..92189273 100644 --- a/src/main/java/grep/neogul_coder/global/auth/service/AuthService.java +++ b/src/main/java/grep/neogul_coder/global/auth/service/AuthService.java @@ -1,10 +1,15 @@ package grep.neogul_coder.global.auth.service; +import grep.neogul_coder.domain.buddy.service.BuddyEnergyService; +import grep.neogul_coder.domain.prtemplate.entity.Link; +import grep.neogul_coder.domain.prtemplate.entity.PrTemplate; +import grep.neogul_coder.domain.prtemplate.repository.LinkRepository; +import grep.neogul_coder.domain.prtemplate.repository.PrTemplateRepository; import grep.neogul_coder.domain.users.entity.User; import grep.neogul_coder.domain.users.exception.UnActivatedUserException; +import grep.neogul_coder.domain.users.exception.UserNotFoundException; import grep.neogul_coder.domain.users.exception.code.UserErrorCode; import grep.neogul_coder.domain.users.repository.UserRepository; -import grep.neogul_coder.domain.users.service.UserService; import grep.neogul_coder.global.auth.code.Role; import grep.neogul_coder.global.auth.entity.RefreshToken; import grep.neogul_coder.global.auth.jwt.JwtTokenProvider; @@ -38,7 +43,9 @@ public class AuthService { private final RefreshTokenService refreshTokenService; private final UserBlackListRepository userBlackListRepository; private final UserRepository usersRepository; - private final UserService userService; + private final LinkRepository linkRepository; + private final PrTemplateRepository prTemplateRepository; + private final BuddyEnergyService buddyEnergyService; public TokenDto signin(LoginRequest loginRequest) { @@ -62,10 +69,6 @@ public TokenDto signin(LoginRequest loginRequest) { return processTokenSignin(authentication.getName(), roles); } - private boolean isUnactivatedUser(String email) { - return !userService.getByEmail(email).getActivated(); - } - public TokenDto processTokenSignin(String email, String roles) { userBlackListRepository.deleteByEmail(email); @@ -98,15 +101,25 @@ public TokenDto processOAuthSignin(OAuth2UserInfo userInfo, String roles) { .oauthId(userInfo.getProviderId()) .password(dummyPassword) .role(Role.ROLE_USER) + .activated(true) .build(); User savedUser = usersRepository.save(newUser); - userService.initializeUserData(savedUser.getId()); + prTemplateRepository.save(PrTemplate.PrTemplateInit(savedUser.getId(), null, null)); + linkRepository.save(Link.LinkInit(savedUser.getId(), null, null)); + linkRepository.save(Link.LinkInit(savedUser.getId(), null, null)); + buddyEnergyService.createDefaultEnergy(savedUser.getId()); return processTokenSignin(savedUser.getEmail(), savedUser.getRole().name()); }); } + private boolean isUnactivatedUser(String email) { + return !usersRepository.findByEmail(email) + .orElseThrow(() -> new UserNotFoundException(UserErrorCode.USER_NOT_FOUND)) + .getActivated(); + } + private boolean isGoogleUser(String email) { User user = usersRepository.findByEmail(email) .orElseThrow(() -> new UsernameNotFoundException("해당 이메일의 유저가 없습니다.")); diff --git a/src/main/java/grep/neogul_coder/global/config/MailConfig.java b/src/main/java/grep/neogul_coder/global/config/MailConfig.java new file mode 100644 index 00000000..4644022c --- /dev/null +++ b/src/main/java/grep/neogul_coder/global/config/MailConfig.java @@ -0,0 +1,45 @@ +package grep.neogul_coder.global.config; + +import java.util.Properties; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +@Configuration +public class MailConfig { + + @Value("${spring.mail.username}") + private String email; + + @Value("${spring.mail.password}") + private String password; + + @Value("${spring.mail.host}") + private String host; + + @Bean + public JavaMailSender mailSender() { + + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(host); + mailSender.setPort(587); + mailSender.setUsername(email); + mailSender.setPassword(password); + + Properties javaMailProperties = new Properties(); + javaMailProperties.put("mail.transport.protocol", "smtp"); + javaMailProperties.put("mail.smtp.auth", "true"); + javaMailProperties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); + javaMailProperties.put("mail.smtp.starttls.enable", "true"); + javaMailProperties.put("mail.debug", "false"); + javaMailProperties.put("mail.smtp.ssl.trust", "smtp.gmail.com"); + javaMailProperties.put("mail.smtp.ssl.protocols", "TLSv1.2"); + + mailSender.setJavaMailProperties(javaMailProperties); + + return mailSender; + } + +} diff --git a/src/main/java/grep/neogul_coder/global/config/security/SecurityConfig.java b/src/main/java/grep/neogul_coder/global/config/security/SecurityConfig.java index f9768a60..b7e57fee 100644 --- a/src/main/java/grep/neogul_coder/global/config/security/SecurityConfig.java +++ b/src/main/java/grep/neogul_coder/global/config/security/SecurityConfig.java @@ -88,7 +88,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, ClientRegistra "/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**", "/webjars/**", "/favicon.ico", - "/error").permitAll() + "/error" + ).permitAll() + .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 From c80ba9b2b4312da94598fb717da89358d050fd15 Mon Sep 17 00:00:00 2001 From: dbrkdgus00 Date: Tue, 22 Jul 2025 19:51:44 +0900 Subject: [PATCH 18/18] =?UTF-8?q?[EA3-111]=20feature=20:=20data.sql=20comm?= =?UTF-8?q?ent=20=EB=AC=B8=20=EC=A3=BC=EC=84=9D=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B7=B8=EB=A3=B9=EC=B1=84=ED=8C=85=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9C=84=ED=95=9C=20html=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/data.sql | 10 +-- src/main/resources/static/Chat-Test.html | 86 ++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 src/main/resources/static/Chat-Test.html diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 98c35ea1..5cd538a3 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -16,11 +16,11 @@ INSERT INTO study_post (study_id, user_id, title, category, content) VALUES (4, INSERT INTO study_post (study_id, user_id, title, category, content) VALUES (2, 2, '개발 유튜브 공유', 'FREE', '재미니의 개발실무 ( 토스 ); 개발바닥'); INSERT INTO study_post (study_id, user_id, title, category, content) VALUES (5, 5, '점심 메뉴 추천', 'FREE', '오늘 점심 뭐먹을지 추천 받습니다!'); -INSERT INTO comment (post_id, user_id, content) VALUES (5, 4, '확인 했습니다!'); -INSERT INTO comment (post_id, user_id, content) VALUES (5, 4, '좋은 정보 감사합니다!'); -INSERT INTO comment (post_id, user_id, content) VALUES (3, 2, '관련된 블로그 공유 드립니다!'); -INSERT INTO comment (post_id, user_id, content) VALUES (2, 5, '정보 감사합니다!'); -INSERT INTO comment (post_id, user_id, content) VALUES (4, 1, '제육 돈까스'); +-- INSERT INTO comment (post_id, user_id, content) VALUES (5, 4, '확인 했습니다!'); +-- INSERT INTO comment (post_id, user_id, content) VALUES (5, 4, '좋은 정보 감사합니다!'); +-- INSERT INTO comment (post_id, user_id, content) VALUES (3, 2, '관련된 블로그 공유 드립니다!'); +-- INSERT INTO comment (post_id, user_id, content) VALUES (2, 5, '정보 감사합니다!'); +-- INSERT INTO comment (post_id, user_id, content) VALUES (4, 1, '제육 돈까스'); INSERT INTO study_member (study_id, user_id, role, participated) VALUES (1, 3, 'LEADER', FALSE); INSERT INTO study_member (study_id, user_id, role, participated) VALUES (1, 4, 'MEMBER', FALSE); diff --git a/src/main/resources/static/Chat-Test.html b/src/main/resources/static/Chat-Test.html new file mode 100644 index 00000000..edb6eaf0 --- /dev/null +++ b/src/main/resources/static/Chat-Test.html @@ -0,0 +1,86 @@ + + + + + 그룹 채팅 로컬 테스트 + + + + +

그룹 채팅 테스트 (로컬 서버)

+ +
+ + + + + + +
+ +
+ +
+ + +
+ +

채팅 로그

+
    + + + +