From be4d5040be72fda12b5ce348ee84277d1361da31 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 19 Jul 2025 22:27:57 +0900 Subject: [PATCH 01/40] =?UTF-8?q?[#20]=20feat:=20crew=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../run/backend/domain/crew/entity/Crew.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main/java/run/backend/domain/crew/entity/Crew.java b/src/main/java/run/backend/domain/crew/entity/Crew.java index 5dd1642..31b8f2d 100644 --- a/src/main/java/run/backend/domain/crew/entity/Crew.java +++ b/src/main/java/run/backend/domain/crew/entity/Crew.java @@ -41,6 +41,22 @@ public class Crew extends BaseEntity { @Column(name = "monthly_score_total") private BigDecimal monthlyScoreTotal; + public void incrementMemberCount() { + this.memberCount++; + } + + public void updateName(String newName) { + this.name = newName; + } + + public void updateDescription(String newDescription) { + this.description = newDescription; + } + + public void updateImage(String newImageName) { + this.image = newImageName; + } + @Builder public Crew ( String name, From 79e04c553f9e6800b263a2b184557466cf435f30 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 19 Jul 2025 22:28:12 +0900 Subject: [PATCH 02/40] =?UTF-8?q?[#20]=20feat:=20crewInfoRequestDto=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../run/backend/domain/crew/dto/request/CrewInfoRequest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/run/backend/domain/crew/dto/request/CrewInfoRequest.java b/src/main/java/run/backend/domain/crew/dto/request/CrewInfoRequest.java index cd64f2d..749b075 100644 --- a/src/main/java/run/backend/domain/crew/dto/request/CrewInfoRequest.java +++ b/src/main/java/run/backend/domain/crew/dto/request/CrewInfoRequest.java @@ -1,4 +1,7 @@ package run.backend.domain.crew.dto.request; -public record CrewInfoRequest() { +public record CrewInfoRequest( + String name, + String description +) { } From 501785942b0f69484d13d59cc4ca52d587b2fd5b Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 19 Jul 2025 22:28:25 +0900 Subject: [PATCH 03/40] =?UTF-8?q?[#20]=20feat:=20crewProfileResponse=20DTO?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/dto/response/CrewProfileResponse.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/run/backend/domain/crew/dto/response/CrewProfileResponse.java b/src/main/java/run/backend/domain/crew/dto/response/CrewProfileResponse.java index ded24a1..b6a8a98 100644 --- a/src/main/java/run/backend/domain/crew/dto/response/CrewProfileResponse.java +++ b/src/main/java/run/backend/domain/crew/dto/response/CrewProfileResponse.java @@ -1,4 +1,14 @@ package run.backend.domain.crew.dto.response; -public record CrewProfileResponse() { +import lombok.Builder; + +@Builder +public record CrewProfileResponse( + String crewImage, + String crewName, + String crewDescription, + Long memberCount, + String leaderImage, + String leaderName +) { } From f974bdc159ebb62d821464d05d71f3b9ceebe0b9 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 19 Jul 2025 22:28:57 +0900 Subject: [PATCH 04/40] =?UTF-8?q?[#20]=20fix:=20=EB=A6=AC=EB=8D=94?= =?UTF-8?q?=EA=B0=80=20join=ED=95=9C=20=EA=B2=BD=EC=9A=B0,=20=EC=9D=BC?= =?UTF-8?q?=EB=B0=98=20=EB=A9=A4=EB=B2=84=EA=B0=80=20Join=ED=95=9C=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=ED=95=B4=EC=84=9C=20joinCrew=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/crew/entity/JoinCrew.java | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/main/java/run/backend/domain/crew/entity/JoinCrew.java b/src/main/java/run/backend/domain/crew/entity/JoinCrew.java index de00b68..5716da9 100644 --- a/src/main/java/run/backend/domain/crew/entity/JoinCrew.java +++ b/src/main/java/run/backend/domain/crew/entity/JoinCrew.java @@ -48,13 +48,38 @@ public void approveJoin() { this.joinStatus = JoinStatus.APPROVED; } + public static JoinCrew createLeaderJoin(Member member, Crew crew) { + return JoinCrew.builder() + .crew(crew) + .member(member) + .role(Role.LEADER) + .joinStatus(JoinStatus.APPROVED) + .joinedDate(LocalDate.now()) + .build(); + } + + public static JoinCrew createAppliedJoin(Member member, Crew crew) { + return JoinCrew.builder() + .crew(crew) + .member(member) + .role(Role.MEMBER) + .joinStatus(JoinStatus.APPLIED) + .build(); + } + @Builder - public JoinCrew( + private JoinCrew( Member member, - Crew crew + Crew crew, + Role role, + JoinStatus joinStatus, + LocalDate joinedDate + ) { this.crew = crew; this.member = member; - this.joinStatus = JoinStatus.APPLIED; + this.role = role; + this.joinStatus = joinStatus; + this.joinedDate = joinedDate; } } From 05e77cbc3e11750b861e5bbaeddcc23474422952 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 19 Jul 2025 22:29:11 +0900 Subject: [PATCH 05/40] =?UTF-8?q?[#20]=20feat:=20member=20role=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=98=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/run/backend/domain/member/entity/Member.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/run/backend/domain/member/entity/Member.java b/src/main/java/run/backend/domain/member/entity/Member.java index a15637f..0b3b21c 100644 --- a/src/main/java/run/backend/domain/member/entity/Member.java +++ b/src/main/java/run/backend/domain/member/entity/Member.java @@ -63,6 +63,10 @@ public void updateImage(String imageName) { this.profileImage = imageName; } + public void updateRole(Role role) { + this.role = role; + } + @Builder public Member(String username, String nickname, Gender gender, int age, String oauthId, OAuthType oauthType, String profileImage) { this.username = username; From aaf2377db5dc75619a261fbeddf6550601a00b62 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 19 Jul 2025 22:29:52 +0900 Subject: [PATCH 06/40] =?UTF-8?q?[#20]=20feat:=20=ED=81=AC=EB=A3=A8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1,=20=ED=81=AC=EB=A3=A8=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=88=98=EC=A0=95,=20=ED=81=AC=EB=A3=A8=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EC=BD=94=EB=93=9C=20=EC=A1=B0=ED=9A=8C,=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A1=9C=20=ED=81=AC=EB=A3=A8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C,=20=ED=81=AC=EB=A3=A8=20=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/controller/CrewController.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/main/java/run/backend/domain/crew/controller/CrewController.java diff --git a/src/main/java/run/backend/domain/crew/controller/CrewController.java b/src/main/java/run/backend/domain/crew/controller/CrewController.java new file mode 100644 index 0000000..2057760 --- /dev/null +++ b/src/main/java/run/backend/domain/crew/controller/CrewController.java @@ -0,0 +1,77 @@ +package run.backend.domain.crew.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import run.backend.domain.crew.dto.common.CrewInviteCodeDto; +import run.backend.domain.crew.dto.request.CrewInfoRequest; +import run.backend.domain.crew.dto.response.CrewProfileResponse; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.crew.service.CrewServiceImpl; +import run.backend.domain.member.entity.Member; +import run.backend.global.annotation.member.Login; +import run.backend.global.common.response.CommonResponse; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/crews") +@Tag(name = "Crews", description = "Crew 관련 API") +public class CrewController { + + private final CrewServiceImpl crewService; + + @Operation(summary = "크루 생성", description = "크루 생성하는 API 입니다.") + @PostMapping + public CommonResponse createCrew( + @Login Member member, + @RequestParam String imageStatus, + @RequestPart(value = "data")CrewInfoRequest data, + @RequestPart(value = "image", required = false) MultipartFile image + ) { + + CrewInviteCodeDto response = crewService.createCrew(member, imageStatus, image, data); + return new CommonResponse<>("크루 생성 성공", response); + } + + @Operation(summary = "크루 정보 수정", description = "크루 정보 수정하는 API 입니다.") + @PatchMapping + public CommonResponse updateCrewInfo( + @Login Member member, + Crew crew, + @RequestParam String imageStatus, + @RequestPart(value = "data")CrewInfoRequest data, + @RequestPart(value = "image", required = false) MultipartFile image + ) { + + crewService.updateCrew(member, crew, imageStatus, image, data); + return new CommonResponse<>("크루 정보 수정 성공"); + } + + @Operation(summary = "크루의 초대 코드 조회", description = "크루의 초대 코드 조회하는 API 입니다.") + @GetMapping("/{crewId}/invite-code") + public CommonResponse getCrewInviteCode(Crew crew) { + + CrewInviteCodeDto response = crewService.getCrewInviteCode(crew); + return new CommonResponse<>("크루 초대 코드 조회 성공", response); + } + + @Operation(summary = "초대 코드로 크루 조회", description = "초대 코드로 크루를 조회하는 API 입니다.") + @GetMapping("/invite-code/{inviteCode}") + public CommonResponse getCrewByInviteCode(@PathVariable String inviteCode) { + + CrewProfileResponse response = crewService.getCrewByInviteCode(inviteCode); + return new CommonResponse<>("크루 조회 성공", response); + } + + @Operation(summary = "크루 가입", description = "크루 가입하는 API 입니다.") + @PostMapping("/{crewId}/join") + public CommonResponse joinCrew( + @Login Member member, + @PathVariable Long crewId) { + + crewService.joinCrew(member, crewId); + return new CommonResponse<>("크루 가입 성공"); + } +} From 161f21ac7a8dbc19888b560e78f26eff7ba05f9c Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 19 Jul 2025 22:30:03 +0900 Subject: [PATCH 07/40] =?UTF-8?q?[#20]=20feat:=20=ED=81=AC=EB=A3=A8=20Exce?= =?UTF-8?q?ption=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/crew/exception/CrewErrorCode.java | 17 ++++++++++++++ .../domain/crew/exception/CrewException.java | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/main/java/run/backend/domain/crew/exception/CrewErrorCode.java create mode 100644 src/main/java/run/backend/domain/crew/exception/CrewException.java diff --git a/src/main/java/run/backend/domain/crew/exception/CrewErrorCode.java b/src/main/java/run/backend/domain/crew/exception/CrewErrorCode.java new file mode 100644 index 0000000..7ba5d9e --- /dev/null +++ b/src/main/java/run/backend/domain/crew/exception/CrewErrorCode.java @@ -0,0 +1,17 @@ +package run.backend.domain.crew.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import run.backend.global.exception.ErrorCode; + +@Getter +@AllArgsConstructor +public enum CrewErrorCode implements ErrorCode { + + ALREADY_JOINED_CREW(7001, "이미 가입한 크루가 있습니다."), + NOT_FOUND_CREW(7002, "해당 크루를 찾을 수 없습니다."); + + + private final int errorCode; + private final String errorMessage; +} diff --git a/src/main/java/run/backend/domain/crew/exception/CrewException.java b/src/main/java/run/backend/domain/crew/exception/CrewException.java new file mode 100644 index 0000000..9853c65 --- /dev/null +++ b/src/main/java/run/backend/domain/crew/exception/CrewException.java @@ -0,0 +1,22 @@ +package run.backend.domain.crew.exception; + +import run.backend.global.exception.CustomException; + +public class CrewException extends CustomException { + + public CrewException(final CrewErrorCode crewErrorCode) { + super(crewErrorCode); + } + + public static class AlreadyJoinedCrew extends CrewException { + public AlreadyJoinedCrew() { + super(CrewErrorCode.ALREADY_JOINED_CREW); + } + } + + public static class NotFoundCrew extends CrewException { + public NotFoundCrew() { + super(CrewErrorCode.NOT_FOUND_CREW); + } + } +} From cbb4c54d1d99ceeddd99e21cf03ba2edceea6e9e Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 19 Jul 2025 22:30:17 +0900 Subject: [PATCH 08/40] =?UTF-8?q?[#20]=20feat:=20crewInviteCodeDTO=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/crew/dto/common/CrewInviteCodeDto.java | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/java/run/backend/domain/crew/dto/common/CrewInviteCodeDto.java diff --git a/src/main/java/run/backend/domain/crew/dto/common/CrewInviteCodeDto.java b/src/main/java/run/backend/domain/crew/dto/common/CrewInviteCodeDto.java new file mode 100644 index 0000000..c09c2d0 --- /dev/null +++ b/src/main/java/run/backend/domain/crew/dto/common/CrewInviteCodeDto.java @@ -0,0 +1,6 @@ +package run.backend.domain.crew.dto.common; + +public record CrewInviteCodeDto( + String inviteCode +) { +} From 577afbc2a00fed6f35cc7e7f32538a49fc0627f9 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 19 Jul 2025 22:30:41 +0900 Subject: [PATCH 09/40] =?UTF-8?q?[#20]=20feat:=20crew,=20joinCrew=20?= =?UTF-8?q?=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=BD=94?= =?UTF-8?q?=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 --- .../crew/repository/CrewRepository.java | 11 +++++++++ .../crew/repository/JoinCrewRepository.java | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/main/java/run/backend/domain/crew/repository/CrewRepository.java create mode 100644 src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java diff --git a/src/main/java/run/backend/domain/crew/repository/CrewRepository.java b/src/main/java/run/backend/domain/crew/repository/CrewRepository.java new file mode 100644 index 0000000..e6992eb --- /dev/null +++ b/src/main/java/run/backend/domain/crew/repository/CrewRepository.java @@ -0,0 +1,11 @@ +package run.backend.domain.crew.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import run.backend.domain.crew.entity.Crew; + +import java.util.Optional; + +public interface CrewRepository extends JpaRepository { + + Optional findByInviteCode(String inviteCode); +} diff --git a/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java b/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java new file mode 100644 index 0000000..b7a3b8d --- /dev/null +++ b/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java @@ -0,0 +1,23 @@ +package run.backend.domain.crew.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.crew.entity.JoinCrew; +import run.backend.domain.crew.enums.JoinStatus; +import run.backend.domain.member.entity.Member; +import run.backend.domain.member.enums.Role; + +public interface JoinCrewRepository extends JpaRepository { + + boolean existsByMemberAndJoinStatus(Member member, JoinStatus joinStatus); + + @Query(""" + SELECT jc.member + FROM JoinCrew jc + JOIN Crew c ON jc.crew = c + WHERE jc.role = :role + """) + Member findCrewLeader(@Param("role") Role role, Crew crew); +} From d61c7bba80c0f511e57d3bad28f0df5bddfba5ee Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 19 Jul 2025 22:30:50 +0900 Subject: [PATCH 10/40] =?UTF-8?q?[#20]=20feat:=20crewService=20=EC=BD=94?= =?UTF-8?q?=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 --- .../domain/crew/service/CrewServiceImpl.java | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java diff --git a/src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java b/src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java new file mode 100644 index 0000000..9c0baf3 --- /dev/null +++ b/src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java @@ -0,0 +1,118 @@ +package run.backend.domain.crew.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import run.backend.domain.crew.dto.common.CrewInviteCodeDto; +import run.backend.domain.crew.dto.request.CrewInfoRequest; +import run.backend.domain.crew.dto.response.CrewProfileResponse; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.crew.entity.JoinCrew; +import run.backend.domain.crew.enums.JoinStatus; +import run.backend.domain.crew.exception.CrewException; +import run.backend.domain.crew.repository.CrewRepository; +import run.backend.domain.crew.repository.JoinCrewRepository; +import run.backend.domain.file.service.FileService; +import run.backend.domain.member.entity.Member; +import run.backend.domain.member.enums.Role; +import run.backend.domain.member.repository.MemberRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CrewServiceImpl implements CrewService { + + private final FileService fileService; + private final CrewRepository crewRepository; + private final MemberRepository memberRepository; + private final JoinCrewRepository joinCrewRepository; + + @Override + @Transactional + public CrewInviteCodeDto createCrew(Member member, String imageStatus, MultipartFile image, CrewInfoRequest data) { + + if (joinCrewRepository.existsByMemberAndJoinStatus(member, JoinStatus.APPROVED)) + throw new CrewException.AlreadyJoinedCrew(); + + // 1. Crew 생성 + String imageName = "default-profile-image.png"; + if (imageStatus.equals("updated")) + imageName = fileService.saveProfileImage(image); + Crew crew = Crew.builder() + .image(imageName) + .name(data.name()) + .description(data.description()) + .build(); + crewRepository.save(crew); + + // 2. JoinCrew 생성 + JoinCrew joinCrew = JoinCrew.createLeaderJoin(member, crew); + joinCrewRepository.save(joinCrew); + + // 3. Member Role LEADER 로 변경 + member.updateRole(Role.LEADER); + memberRepository.save(member); + + return new CrewInviteCodeDto(crew.getInviteCode()); + } + + @Override + @Transactional + public void updateCrew(Member member, Crew crew, String imageStatus, MultipartFile image, CrewInfoRequest data) { + + switch (imageStatus) { + + case "updated" : + fileService.deleteImage(crew.getImage()); + String newImageName = fileService.saveProfileImage(image); + crew.updateImage(newImageName); + break; + case "removed" : + fileService.deleteImage(crew.getImage()); + crew.updateImage("default-profile-image.png"); + break; + } + + if (data.name() != null) + crew.updateName(data.name()); + if (data.description() != null) + crew.updateDescription(data.description()); + + crewRepository.save(crew); + } + + @Override + public CrewInviteCodeDto getCrewInviteCode(Crew crew) { + + return new CrewInviteCodeDto(crew.getInviteCode()); + } + + @Override + public CrewProfileResponse getCrewByInviteCode(String inviteCode) { + + Crew crew = crewRepository.findByInviteCode(inviteCode) + .orElseThrow(CrewException.NotFoundCrew::new); + Member leader = joinCrewRepository.findCrewLeader(Role.LEADER, crew); + + return CrewProfileResponse.builder() + .crewImage(crew.getImage()) + .crewName(crew.getName()) + .crewDescription(crew.getDescription()) + .memberCount(crew.getMemberCount()) + .leaderImage(leader.getProfileImage()) + .leaderName(leader.getNickname()) + .build(); + } + + @Override + @Transactional + public void joinCrew(Member member, Long crewId) { + + Crew crew = crewRepository.findById(crewId) + .orElseThrow(); + + JoinCrew joinCrew = JoinCrew.createAppliedJoin(member, crew); + joinCrewRepository.save(joinCrew); + } +} From 987a975d53307b9812533dab7010e54a03c187d6 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 19 Jul 2025 22:31:05 +0900 Subject: [PATCH 11/40] =?UTF-8?q?[#20]=20feat:=20crew=EC=97=90=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/crew/service/CrewService.java | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/main/java/run/backend/domain/crew/service/CrewService.java b/src/main/java/run/backend/domain/crew/service/CrewService.java index f28d31e..04fbfe4 100644 --- a/src/main/java/run/backend/domain/crew/service/CrewService.java +++ b/src/main/java/run/backend/domain/crew/service/CrewService.java @@ -1,26 +1,38 @@ package run.backend.domain.crew.service; +import org.springframework.web.multipart.MultipartFile; +import run.backend.domain.crew.dto.common.CrewInviteCodeDto; import run.backend.domain.crew.dto.request.CrewInfoRequest; import run.backend.domain.crew.dto.response.*; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.member.entity.Member; import run.backend.domain.member.enums.Role; import java.time.YearMonth; public interface CrewService { - void updateCrew(CrewInfoRequest crewInfoRequest); + CrewInviteCodeDto createCrew(Member member, String imageStatus, MultipartFile image, CrewInfoRequest crewInfoRequest); - CrewSearchResponse searchCrew(String crewName); + void updateCrew(Member member, Crew crew, String imageStatus, MultipartFile image, CrewInfoRequest crewInfoRequest); - CrewInfoResponse getCrewInfo(Long crewId); + CrewInviteCodeDto getCrewInviteCode(Crew crew); - CrewMonthlyCanlendarResponse getCrewMonthlyCalendar(Long crewId, YearMonth yearMonth); + CrewProfileResponse getCrewByInviteCode(String inviteCode); - CrewUpcomingEventResponse getUpcomingEvents(Long crewId); + void joinCrew(Member member, Long crewId); - CrewMemberResponse getCrewMemberProfile(Long crewId); - - void updateCrewMemberRole(Long memberId, Role role); - - CrewSearchResponse getRankCrew(); +// CrewSearchResponse searchCrew(String crewName); +// +// CrewInfoResponse getCrewInfo(Long crewId); +// +// CrewMonthlyCanlendarResponse getCrewMonthlyCalendar(Long crewId, YearMonth yearMonth); +// +// CrewUpcomingEventResponse getUpcomingEvents(Long crewId); +// +// CrewMemberResponse getCrewMemberProfile(Long crewId); +// +// void updateCrewMemberRole(Long memberId, Role role); +// +// CrewSearchResponse getRankCrew(); } From 4d1b1e575c6df70cdd7f211e304aec29904bdea1 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 19 Jul 2025 22:54:03 +0900 Subject: [PATCH 12/40] =?UTF-8?q?[#20]=20chore:=20ddl-auto=20update?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2af2f57..8c40a93 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -17,7 +17,7 @@ spring: jpa: database: mysql hibernate: - ddl-auto: create + ddl-auto: update properties: hibernate: dialect: org.hibernate.dialect.MySQLDialect From 63e673df9fb0a939d61028bb1846bbfbf193b9bb Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 19 Jul 2025 22:54:34 +0900 Subject: [PATCH 13/40] =?UTF-8?q?[#20]=20feat:=20MemberCrew=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../run/backend/domain/crew/controller/CrewController.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/run/backend/domain/crew/controller/CrewController.java b/src/main/java/run/backend/domain/crew/controller/CrewController.java index 2057760..3620415 100644 --- a/src/main/java/run/backend/domain/crew/controller/CrewController.java +++ b/src/main/java/run/backend/domain/crew/controller/CrewController.java @@ -12,6 +12,7 @@ import run.backend.domain.crew.service.CrewServiceImpl; import run.backend.domain.member.entity.Member; import run.backend.global.annotation.member.Login; +import run.backend.global.annotation.member.MemberCrew; import run.backend.global.common.response.CommonResponse; @RestController @@ -39,7 +40,7 @@ public CommonResponse createCrew( @PatchMapping public CommonResponse updateCrewInfo( @Login Member member, - Crew crew, + @MemberCrew Crew crew, @RequestParam String imageStatus, @RequestPart(value = "data")CrewInfoRequest data, @RequestPart(value = "image", required = false) MultipartFile image @@ -51,7 +52,7 @@ public CommonResponse updateCrewInfo( @Operation(summary = "크루의 초대 코드 조회", description = "크루의 초대 코드 조회하는 API 입니다.") @GetMapping("/{crewId}/invite-code") - public CommonResponse getCrewInviteCode(Crew crew) { + public CommonResponse getCrewInviteCode(@MemberCrew Crew crew) { CrewInviteCodeDto response = crewService.getCrewInviteCode(crew); return new CommonResponse<>("크루 초대 코드 조회 성공", response); From 66229bf5589761099ccd88ca313842201896d0da Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 19 Jul 2025 22:54:42 +0900 Subject: [PATCH 14/40] =?UTF-8?q?[#20]=20feat:=20dev=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=9E=91=20=EB=A8=B8=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/crew/repository/JoinCrewRepository.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java b/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java index 0039193..28561dc 100644 --- a/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java +++ b/src/main/java/run/backend/domain/crew/repository/JoinCrewRepository.java @@ -10,11 +10,6 @@ import run.backend.domain.member.entity.Member; import run.backend.domain.member.enums.Role; import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import run.backend.domain.crew.entity.JoinCrew; -import run.backend.domain.crew.enums.JoinStatus; import run.backend.domain.event.dto.response.EventCreationValidationDto; public interface JoinCrewRepository extends JpaRepository { @@ -38,9 +33,9 @@ Optional findByMemberIdAndJoinStatus(@Param("memberId") Long memberId, requesterJoin.crew, captainJoin.member ) - FROM JoinCrew requesterJoin + FROM JoinCrew requesterJoin INNER JOIN JoinCrew captainJoin ON requesterJoin.crew.id = captainJoin.crew.id - WHERE requesterJoin.member.id = :requesterId + WHERE requesterJoin.member.id = :requesterId AND requesterJoin.joinStatus = :status AND captainJoin.member.id = :runningCaptainId AND captainJoin.joinStatus = :status From 517be235261dbfa628d5765b22d81b183646b70b Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 20 Jul 2025 01:09:30 +0900 Subject: [PATCH 15/40] =?UTF-8?q?[#20]=20feat:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=A0=84=EC=97=AD=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=EA=B8=B0=EC=97=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../run/backend/domain/crew/service/CrewServiceImpl.java | 4 +++- .../backend/global/exception/GlobalExceptionHandler.java | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java b/src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java index 9c0baf3..e185cb8 100644 --- a/src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java +++ b/src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java @@ -110,7 +110,9 @@ public CrewProfileResponse getCrewByInviteCode(String inviteCode) { public void joinCrew(Member member, Long crewId) { Crew crew = crewRepository.findById(crewId) - .orElseThrow(); + .orElseThrow(CrewException.NotFoundCrew::new); + if (joinCrewRepository.existsByMemberAndJoinStatus(member, JoinStatus.APPROVED)) + throw new CrewException.AlreadyJoinedCrew(); JoinCrew joinCrew = JoinCrew.createAppliedJoin(member, crew); joinCrewRepository.save(joinCrew); diff --git a/src/main/java/run/backend/global/exception/GlobalExceptionHandler.java b/src/main/java/run/backend/global/exception/GlobalExceptionHandler.java index aba91ea..0187086 100644 --- a/src/main/java/run/backend/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/run/backend/global/exception/GlobalExceptionHandler.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import run.backend.domain.auth.exception.AuthException; +import run.backend.domain.crew.exception.CrewException; import run.backend.domain.file.exception.FileException; import run.backend.domain.member.exception.MemberException; import run.backend.global.common.response.CommonResponse; @@ -20,7 +21,8 @@ public class GlobalExceptionHandler { @ExceptionHandler({ AuthException.RefreshTokenNotFound.class, FileException.FileNotFound.class, - MemberException.MemberNotJoinedCrew.class + MemberException.MemberNotJoinedCrew.class, + CrewException.NotFoundCrew.class }) public ResponseEntity> handleNotFound(final CustomException e) { @@ -36,7 +38,8 @@ public ResponseEntity> handleNotFound(final CustomException FileException.FileSizeExceeded.class, FileException.InvalidFileName.class, FileException.InvalidFileExtension.class, - FileException.InvalidFileType.class + FileException.InvalidFileType.class, + CrewException.AlreadyJoinedCrew.class }) public ResponseEntity> handleConflict(final CustomException e) { From b6fec8d74b6a7e3d41d87082686437548f64deb2 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 20 Jul 2025 01:09:56 +0900 Subject: [PATCH 16/40] =?UTF-8?q?[#20]=20test:=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(=ED=81=AC=EB=A3=A8=20=EC=83=9D=EC=84=B1,?= =?UTF-8?q?=20=ED=81=AC=EB=A3=A8=20=EC=88=98=EC=A0=95,=20=ED=81=AC?= =?UTF-8?q?=EB=A3=A8=20=EA=B0=80=EC=9E=85=20=EC=9A=94=EC=B2=AD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/crew/service/CrewServiceTest.java | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 src/test/java/run/backend/domain/crew/service/CrewServiceTest.java diff --git a/src/test/java/run/backend/domain/crew/service/CrewServiceTest.java b/src/test/java/run/backend/domain/crew/service/CrewServiceTest.java new file mode 100644 index 0000000..95cde3b --- /dev/null +++ b/src/test/java/run/backend/domain/crew/service/CrewServiceTest.java @@ -0,0 +1,250 @@ +package run.backend.domain.crew.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.multipart.MultipartFile; +import run.backend.domain.crew.dto.common.CrewInviteCodeDto; +import run.backend.domain.crew.dto.request.CrewInfoRequest; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.crew.entity.JoinCrew; +import run.backend.domain.crew.enums.JoinStatus; +import run.backend.domain.crew.exception.CrewException; +import run.backend.domain.crew.repository.CrewRepository; +import run.backend.domain.crew.repository.JoinCrewRepository; +import run.backend.domain.file.service.FileService; +import run.backend.domain.member.entity.Member; +import run.backend.domain.member.repository.MemberRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + + +@ExtendWith(MockitoExtension.class) +public class CrewServiceTest { + + @InjectMocks + private CrewServiceImpl crewService; + + @Mock + private FileService fileService; + + @Mock + private JoinCrewRepository joinCrewRepository; + + @Mock + private CrewRepository crewRepository; + + @Mock + private MemberRepository memberRepository; + + @Mock + private Crew crew; + + private Member member; + private CrewInfoRequest request; + + @BeforeEach + void setUp() { + member = Member.builder().username("테스트 유저").build(); + request = new CrewInfoRequest("러너스", "러너스 크루입니다."); + } + + @Nested + @DisplayName("createCrew 메서드는") + class createCrewTest { + + @Test + @DisplayName("이미 크루에 가입된 회원이 크루 생성을 시도하면 예외가 발생한다.") + void throwsException_whenMemberAlreadyInCrew() { + + // given + when(joinCrewRepository.existsByMemberAndJoinStatus(member, JoinStatus.APPROVED)) + .thenReturn(true); + + // when + then + assertThatThrownBy(() -> + crewService.createCrew(member, "unchanged", null, request)) + .isInstanceOf(CrewException.AlreadyJoinedCrew.class); + } + + @Test + @DisplayName("크루 생성 시 응답으로 invite-code가 포함되어야 한다.") + void respondsWithInviteCode_whenCreatingCrew() { + + // given + when(joinCrewRepository.existsByMemberAndJoinStatus(member, JoinStatus.APPROVED)) + .thenReturn(false); + when(crewRepository.save(any(Crew.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(joinCrewRepository.save(any(JoinCrew.class))).thenReturn(null); + when(memberRepository.save(any())).thenReturn(member); + + // when + CrewInviteCodeDto response = crewService.createCrew(member, "unchanged", null, request); + + // then + assertThat(response.inviteCode()).isNotNull(); + } + + @Test + @DisplayName("크루 생성 시 joinCrew가 저장된다.") + void saveJoinCrew_whenCreatingCrew() { + + // given + when(joinCrewRepository.existsByMemberAndJoinStatus(member, JoinStatus.APPROVED)).thenReturn(false); + when(crewRepository.save(any(Crew.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(joinCrewRepository.save(any(JoinCrew.class))).thenReturn(null); + when(memberRepository.save(any(Member.class))).thenReturn(member); + + // when + crewService.createCrew(member, "unchanged", null, request); + + // then + verify(joinCrewRepository).save(any(JoinCrew.class)); + } + + @Test + @DisplayName("크루 생성 시 생성자의 역할이 LEADER로 변경해서 저장한다.") + void updatesMemberRoleToLeader_whenCreatingCrew() { + + // given + when(joinCrewRepository.existsByMemberAndJoinStatus(member, JoinStatus.APPROVED)).thenReturn(false); + when(crewRepository.save(any(Crew.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(joinCrewRepository.save(any(JoinCrew.class))).thenReturn(null); + when(memberRepository.save(any(Member.class))).thenReturn(member); + + // when + crewService.createCrew(member, "unchanged", null, request); + + // then + verify(memberRepository).save(any(Member.class)); + } + } + + @Nested + @DisplayName("updateCrew 메서드는") + class updateCrewTest { + + @Test + @DisplayName("imageStatus가 updated인 경우 기존 이미지를 삭제하고 새 이미지를 저장한다.") + void updateImage_whenImageStatusIsUpdated() { + + // given + String oldImageName = "old_image.png"; + String newImageName = "new_image.png"; + + when(crew.getImage()).thenReturn(oldImageName); // when 안에 Mock 객체 + when(fileService.saveProfileImage(any())).thenReturn(newImageName); + + // when + crewService.updateCrew(member, crew, "updated", mock(MultipartFile.class), request); + + // then + verify(fileService).deleteImage(oldImageName); + verify(fileService).saveProfileImage(any()); + verify(crew).updateImage(newImageName); + verify(crewRepository).save(crew); + } + + @Test + @DisplayName("imageStatus가 removed인 경우 기존 이미지를 삭제하고 기본 이미지를 저장한다.") + void removeImage_whenImageStatusIsRemoved() { + + // given + String oldImageName = "old_image.png"; + when(crew.getImage()).thenReturn(oldImageName); + + // when + crewService.updateCrew(member, crew, "removed", mock(MultipartFile.class), request); + + // then + verify(fileService).deleteImage(oldImageName); + verify(crew).updateImage("default-profile-image.png"); + verify(crewRepository).save(crew); + } + + @Test + @DisplayName("name이 null이 아니면 이름을 업데이트한다.") + void updateName_whenNameIsNotNull() { + + // when + crewService.updateCrew(member, crew, "unchanged", null, request); + + // then + verify(crew).updateName("러너스"); + verify(crewRepository).save(crew); + + } + + @Test + @DisplayName("description null이 아니면 설명을 업데이트한다.") + void updateDescription_whenDescriptionIsNotNull() { + + // when + crewService.updateCrew(member, crew, "unchanged", null, request); + + // then + verify(crew).updateDescription("러너스 크루입니다."); + verify(crewRepository).save(crew); + } + } + + @Nested + @DisplayName("joinCrew 메서드는") + class joinCrewTest { + + @Test + @DisplayName("존재하지 않는 crew id이면 예외가 발생한다.") + void throwException_whenCrewNotFound() { + + // given + Long crewId = 2L; + when(crewRepository.findById(crewId)).thenReturn(Optional.empty()); + + // when + then + assertThatThrownBy(() -> crewService.joinCrew(member, crewId)) + .isInstanceOf(CrewException.NotFoundCrew.class); + } + + @Test + @DisplayName("이미 크루에 가입한 회원이 크루 가입 시도를 하면 예외가 발생한다.") + void throwException_whenMemberAlreadyJoinedCrew() { + + // given + Long crewId = 2L; + when(crewRepository.findById(crewId)).thenReturn(Optional.of(crew)); + when(joinCrewRepository.existsByMemberAndJoinStatus(member, JoinStatus.APPROVED)) + .thenReturn(true); + + // when + then + assertThatThrownBy(() -> crewService.joinCrew(member, crewId)) + .isInstanceOf(CrewException.AlreadyJoinedCrew.class); + } + + @Test + @DisplayName("정상적으로 crew에 가입 신청을 저장한다.") + void saveJoinCrew_whenValidCrewIdGiven() { + + // given + Long crewId = 1L; + when(crewRepository.findById(crewId)).thenReturn(Optional.of(crew)); + when(joinCrewRepository.save(any(JoinCrew.class))).thenReturn(null); + + // when + crewService.joinCrew(member, crewId); + + // then + verify(crewRepository).findById(crewId); + verify(joinCrewRepository).save(any(JoinCrew.class)); + } + } +} From a86a95f8168dbf482bc56ad58eba9ed3b0b420f6 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 20 Jul 2025 01:13:33 +0900 Subject: [PATCH 17/40] =?UTF-8?q?[#20]=20refact:=20Operation=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/crew/controller/CrewController.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/run/backend/domain/crew/controller/CrewController.java b/src/main/java/run/backend/domain/crew/controller/CrewController.java index 3620415..5609a3f 100644 --- a/src/main/java/run/backend/domain/crew/controller/CrewController.java +++ b/src/main/java/run/backend/domain/crew/controller/CrewController.java @@ -23,8 +23,8 @@ public class CrewController { private final CrewServiceImpl crewService; - @Operation(summary = "크루 생성", description = "크루 생성하는 API 입니다.") @PostMapping + @Operation(summary = "크루 생성", description = "크루 생성하는 API 입니다.") public CommonResponse createCrew( @Login Member member, @RequestParam String imageStatus, @@ -36,8 +36,8 @@ public CommonResponse createCrew( return new CommonResponse<>("크루 생성 성공", response); } - @Operation(summary = "크루 정보 수정", description = "크루 정보 수정하는 API 입니다.") @PatchMapping + @Operation(summary = "크루 정보 수정", description = "크루 정보 수정하는 API 입니다.") public CommonResponse updateCrewInfo( @Login Member member, @MemberCrew Crew crew, @@ -50,24 +50,24 @@ public CommonResponse updateCrewInfo( return new CommonResponse<>("크루 정보 수정 성공"); } - @Operation(summary = "크루의 초대 코드 조회", description = "크루의 초대 코드 조회하는 API 입니다.") @GetMapping("/{crewId}/invite-code") + @Operation(summary = "크루의 초대 코드 조회", description = "크루의 초대 코드 조회하는 API 입니다.") public CommonResponse getCrewInviteCode(@MemberCrew Crew crew) { CrewInviteCodeDto response = crewService.getCrewInviteCode(crew); return new CommonResponse<>("크루 초대 코드 조회 성공", response); } - @Operation(summary = "초대 코드로 크루 조회", description = "초대 코드로 크루를 조회하는 API 입니다.") @GetMapping("/invite-code/{inviteCode}") + @Operation(summary = "초대 코드로 크루 조회", description = "초대 코드로 크루를 조회하는 API 입니다.") public CommonResponse getCrewByInviteCode(@PathVariable String inviteCode) { CrewProfileResponse response = crewService.getCrewByInviteCode(inviteCode); return new CommonResponse<>("크루 조회 성공", response); } - @Operation(summary = "크루 가입", description = "크루 가입하는 API 입니다.") @PostMapping("/{crewId}/join") + @Operation(summary = "크루 가입", description = "크루 가입하는 API 입니다.") public CommonResponse joinCrew( @Login Member member, @PathVariable Long crewId) { From e91602ca972b74d8d31434c0ac1836d7d1fc123d Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 20 Jul 2025 01:14:17 +0900 Subject: [PATCH 18/40] =?UTF-8?q?[#20]=20feat:=20=ED=81=AC=EB=A3=A8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=20API=EC=97=90=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/run/backend/domain/crew/controller/CrewController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/run/backend/domain/crew/controller/CrewController.java b/src/main/java/run/backend/domain/crew/controller/CrewController.java index 5609a3f..15b2205 100644 --- a/src/main/java/run/backend/domain/crew/controller/CrewController.java +++ b/src/main/java/run/backend/domain/crew/controller/CrewController.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import run.backend.domain.crew.dto.common.CrewInviteCodeDto; @@ -37,6 +38,7 @@ public CommonResponse createCrew( } @PatchMapping + @PreAuthorize("hasRole('MANAGER') or hasRole('LEADER')") @Operation(summary = "크루 정보 수정", description = "크루 정보 수정하는 API 입니다.") public CommonResponse updateCrewInfo( @Login Member member, From bfb2e19763d6f36d8cafbeb0846d18173f8e8435 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 20 Jul 2025 01:15:34 +0900 Subject: [PATCH 19/40] =?UTF-8?q?[#20]=20feat:=20SecurityConfig=EC=97=90?= =?UTF-8?q?=20crews=20api=20=ED=97=88=EC=9A=A9=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/run/backend/global/security/SecurityConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/run/backend/global/security/SecurityConfig.java b/src/main/java/run/backend/global/security/SecurityConfig.java index 296c76f..da01c0a 100644 --- a/src/main/java/run/backend/global/security/SecurityConfig.java +++ b/src/main/java/run/backend/global/security/SecurityConfig.java @@ -49,7 +49,8 @@ public class SecurityConfig { private final String[] PermitAllPatterns = { "/api/v1/members/**", "/api/v1/auth/**", - "/api/v1/events/**" + "/api/v1/events/**", + "/api/v1/crews/**" }; @Bean From 9108a202052eb92820b1c2d99fea7adc861e8292 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 20 Jul 2025 01:45:32 +0900 Subject: [PATCH 20/40] =?UTF-8?q?[#20]=20refact:=20CrewProfileResponse=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=EC=97=90=EC=84=9C=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../run/backend/domain/crew/service/CrewServiceImpl.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java b/src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java index e185cb8..0693223 100644 --- a/src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java +++ b/src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java @@ -95,14 +95,7 @@ public CrewProfileResponse getCrewByInviteCode(String inviteCode) { .orElseThrow(CrewException.NotFoundCrew::new); Member leader = joinCrewRepository.findCrewLeader(Role.LEADER, crew); - return CrewProfileResponse.builder() - .crewImage(crew.getImage()) - .crewName(crew.getName()) - .crewDescription(crew.getDescription()) - .memberCount(crew.getMemberCount()) - .leaderImage(leader.getProfileImage()) - .leaderName(leader.getNickname()) - .build(); + return CrewProfileResponse.of(crew, leader); } @Override From 672e0ae1b6b663b4331ee872371794b811a32465 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 20 Jul 2025 01:45:42 +0900 Subject: [PATCH 21/40] =?UTF-8?q?[#20]=20refact:=20CrewProfileResponse=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=EC=97=90=EC=84=9C=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/dto/response/CrewProfileResponse.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/run/backend/domain/crew/dto/response/CrewProfileResponse.java b/src/main/java/run/backend/domain/crew/dto/response/CrewProfileResponse.java index b6a8a98..629e9dc 100644 --- a/src/main/java/run/backend/domain/crew/dto/response/CrewProfileResponse.java +++ b/src/main/java/run/backend/domain/crew/dto/response/CrewProfileResponse.java @@ -1,6 +1,8 @@ package run.backend.domain.crew.dto.response; import lombok.Builder; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.member.entity.Member; @Builder public record CrewProfileResponse( @@ -11,4 +13,16 @@ public record CrewProfileResponse( String leaderImage, String leaderName ) { + + public static CrewProfileResponse of(Crew crew, Member leader) { + + return new CrewProfileResponse( + crew.getImage(), + crew.getName(), + crew.getDescription(), + crew.getMemberCount(), + leader.getProfileImage(), + leader.getNickname() + ); + } } From ab9ec57e327a78323cde8b8262f9eba886d83773 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 20 Jul 2025 01:46:02 +0900 Subject: [PATCH 22/40] =?UTF-8?q?[#20]=20test:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EB=AA=A9=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/run/backend/domain/crew/service/CrewServiceTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/run/backend/domain/crew/service/CrewServiceTest.java b/src/test/java/run/backend/domain/crew/service/CrewServiceTest.java index 95cde3b..b87a094 100644 --- a/src/test/java/run/backend/domain/crew/service/CrewServiceTest.java +++ b/src/test/java/run/backend/domain/crew/service/CrewServiceTest.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.*; +@DisplayName("Crew 서비스 테스트") @ExtendWith(MockitoExtension.class) public class CrewServiceTest { From a45d32a74211c218e0e5b009c6007cb5943bd720 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 20 Jul 2025 01:46:14 +0900 Subject: [PATCH 23/40] =?UTF-8?q?[#20]=20test:=20Crew,=20JoinCrew=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/crew/entity/CrewTest.java | 71 +++++++++++++++++++ .../domain/crew/entity/JoinCrewTest.java | 49 +++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 src/test/java/run/backend/domain/crew/entity/CrewTest.java create mode 100644 src/test/java/run/backend/domain/crew/entity/JoinCrewTest.java diff --git a/src/test/java/run/backend/domain/crew/entity/CrewTest.java b/src/test/java/run/backend/domain/crew/entity/CrewTest.java new file mode 100644 index 0000000..fad317b --- /dev/null +++ b/src/test/java/run/backend/domain/crew/entity/CrewTest.java @@ -0,0 +1,71 @@ +package run.backend.domain.crew.entity; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Crew 도메인 테스트") +public class CrewTest { + + @Test + @DisplayName("이름을 변경하면 name 필드가 변경된다.") + void updateName() { + + // given + Crew crew = new Crew(); + String newName = "크루 새로운 이름"; + + // when + crew.updateName(newName); + + // then + assertThat(crew.getName()).isEqualTo(newName); + } + + @Test + @DisplayName("설명을 변경하면 description 필드가 변경된다.") + void updateDescription() { + + // given + Crew crew = new Crew(); + String newDescription = "새로운 설명"; + + // when + crew.updateDescription(newDescription); + + // then + assertThat(crew.getDescription()).isEqualTo(newDescription); + } + + @Test + @DisplayName("사진을 변경하면 image 필드가 변경된다.") + void updateImage() { + + // given + Crew crew = new Crew(); + String newImage = "image123.png"; + + // when + crew.updateImage(newImage); + + // then + assertThat(crew.getImage()).isEqualTo(newImage); + } + + @Test + @DisplayName("crew를 생성하면 invite-code가 자동으로 생성된다.") + void generateInviteCode_whenCrewCreated() { + + // when + Crew crew = Crew.builder() + .name("테스트 크루") + .description("설명") + .image("image.png") + .build(); + + // then + assertThat(crew.getInviteCode()).isNotNull(); + assertThat(crew.getInviteCode()).isNotBlank(); + } +} diff --git a/src/test/java/run/backend/domain/crew/entity/JoinCrewTest.java b/src/test/java/run/backend/domain/crew/entity/JoinCrewTest.java new file mode 100644 index 0000000..1cb3dd0 --- /dev/null +++ b/src/test/java/run/backend/domain/crew/entity/JoinCrewTest.java @@ -0,0 +1,49 @@ +package run.backend.domain.crew.entity; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import run.backend.domain.crew.enums.JoinStatus; +import run.backend.domain.member.entity.Member; +import run.backend.domain.member.enums.Role; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +@DisplayName("JoinCrew 도메인 테스트") +public class JoinCrewTest { + + Member member = mock(Member.class); + Crew crew = mock(Crew.class); + + @Test + @DisplayName("리더가 JoinCrew 생성 시 role은 LEADER, 상태는 APPROVED, joinedDate는 오늘이다.") + void createLeaderJoin_setsCorrectValues() { + + // when + JoinCrew joinCrew = JoinCrew.createLeaderJoin(member, crew); + + // then + assertThat(joinCrew.getMember()).isEqualTo(member); + assertThat(joinCrew.getCrew()).isEqualTo(crew); + assertThat(joinCrew.getRole()).isEqualTo(Role.LEADER); + assertThat(joinCrew.getJoinStatus()).isEqualTo(JoinStatus.APPROVED); + assertThat(joinCrew.getJoinedDate()).isEqualTo(LocalDate.now()); + } + + @Test + @DisplayName("일반 회원이 JoinCrew 생성 시 role은 MEMBER, 상태는 APPLIED, joinedDate는 null이다.") + void createAppliedJoin_setsCorrectValues() { + + // when + JoinCrew joinCrew = JoinCrew.createAppliedJoin(member, crew); + + // then + assertThat(joinCrew.getMember()).isEqualTo(member); + assertThat(joinCrew.getCrew()).isEqualTo(crew); + assertThat(joinCrew.getRole()).isEqualTo(Role.MEMBER); + assertThat(joinCrew.getJoinStatus()).isEqualTo(JoinStatus.APPLIED); + assertThat(joinCrew.getJoinedDate()).isNull(); + } +} From dd4291bd95c7af74325763ad37ed5ccfe4aa18bb Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 20 Jul 2025 16:38:30 +0900 Subject: [PATCH 24/40] =?UTF-8?q?[#20]=20feat:=20CrewBaseInfoResponse=20DT?= =?UTF-8?q?O=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/CrewBaseInfoResponse.java | 33 +++++++++++++++++++ .../crew/dto/response/CrewInfoResponse.java | 4 --- 2 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 src/main/java/run/backend/domain/crew/dto/response/CrewBaseInfoResponse.java delete mode 100644 src/main/java/run/backend/domain/crew/dto/response/CrewInfoResponse.java diff --git a/src/main/java/run/backend/domain/crew/dto/response/CrewBaseInfoResponse.java b/src/main/java/run/backend/domain/crew/dto/response/CrewBaseInfoResponse.java new file mode 100644 index 0000000..b333bc7 --- /dev/null +++ b/src/main/java/run/backend/domain/crew/dto/response/CrewBaseInfoResponse.java @@ -0,0 +1,33 @@ +package run.backend.domain.crew.dto.response; + +import lombok.Builder; +import run.backend.domain.crew.entity.Crew; + +import java.math.BigDecimal; + +@Builder +public record CrewBaseInfoResponse( + + int rank, + String image, + String name, + String description, + Long memberCount, + BigDecimal monthlyDistanceTotal, + Long monthlyTimeTotal + +) { + + public static CrewBaseInfoResponse of(int rank, Crew crew) { + + return CrewBaseInfoResponse.builder() + .rank(rank) + .image(crew.getImage()) + .name(crew.getName()) + .description(crew.getDescription()) + .memberCount(crew.getMemberCount()) + .monthlyDistanceTotal(crew.getMonthlyDistanceTotal()) + .monthlyTimeTotal(crew.getMonthlyTimeTotal()) + .build(); + } +} diff --git a/src/main/java/run/backend/domain/crew/dto/response/CrewInfoResponse.java b/src/main/java/run/backend/domain/crew/dto/response/CrewInfoResponse.java deleted file mode 100644 index 3c56000..0000000 --- a/src/main/java/run/backend/domain/crew/dto/response/CrewInfoResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package run.backend.domain.crew.dto.response; - -public record CrewInfoResponse() { -} From c5f6bb074762d260b623e5fb68b79e108ad71be6 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 20 Jul 2025 16:38:52 +0900 Subject: [PATCH 25/40] =?UTF-8?q?[#20]=20delete:=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=EC=97=86=EB=8A=94=20DTO=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/crew/dto/response/CrewEventResponse.java | 4 ---- .../backend/domain/crew/dto/response/CrewRecordResponse.java | 4 ---- 2 files changed, 8 deletions(-) delete mode 100644 src/main/java/run/backend/domain/crew/dto/response/CrewEventResponse.java delete mode 100644 src/main/java/run/backend/domain/crew/dto/response/CrewRecordResponse.java diff --git a/src/main/java/run/backend/domain/crew/dto/response/CrewEventResponse.java b/src/main/java/run/backend/domain/crew/dto/response/CrewEventResponse.java deleted file mode 100644 index 7ff7636..0000000 --- a/src/main/java/run/backend/domain/crew/dto/response/CrewEventResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package run.backend.domain.crew.dto.response; - -public record CrewEventResponse() { -} diff --git a/src/main/java/run/backend/domain/crew/dto/response/CrewRecordResponse.java b/src/main/java/run/backend/domain/crew/dto/response/CrewRecordResponse.java deleted file mode 100644 index cafedae..0000000 --- a/src/main/java/run/backend/domain/crew/dto/response/CrewRecordResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package run.backend.domain.crew.dto.response; - -public record CrewRecordResponse() { -} From 7777e8c68a791f26f6cc08491f09162583377679 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 20 Jul 2025 16:39:34 +0900 Subject: [PATCH 26/40] =?UTF-8?q?[#20]=20feat:=20=EA=B8=B0=EA=B0=84?= =?UTF-8?q?=EB=B3=84=20=EC=9D=BC=EC=A0=95=20=EC=A1=B0=ED=9A=8C=ED=95=98?= =?UTF-8?q?=EB=8A=94=20DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/crew/dto/common/DayStatusDto.java | 9 +++++++++ .../response/CrewMonthlyCanlendarResponse.java | 7 ++++--- .../dto/response/CrewWeeklyEventResponse.java | 13 +++++++++++++ .../crew/dto/response/EventProfileResponse.java | 16 +++++++++++++++- .../domain/event/enums/RunningStatus.java | 14 ++++++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 src/main/java/run/backend/domain/crew/dto/common/DayStatusDto.java create mode 100644 src/main/java/run/backend/domain/crew/dto/response/CrewWeeklyEventResponse.java create mode 100644 src/main/java/run/backend/domain/event/enums/RunningStatus.java diff --git a/src/main/java/run/backend/domain/crew/dto/common/DayStatusDto.java b/src/main/java/run/backend/domain/crew/dto/common/DayStatusDto.java new file mode 100644 index 0000000..6cb81e9 --- /dev/null +++ b/src/main/java/run/backend/domain/crew/dto/common/DayStatusDto.java @@ -0,0 +1,9 @@ +package run.backend.domain.crew.dto.common; + +import run.backend.domain.event.enums.RunningStatus; + +public record DayStatusDto( + RunningStatus status, + Long eventId +) { +} diff --git a/src/main/java/run/backend/domain/crew/dto/response/CrewMonthlyCanlendarResponse.java b/src/main/java/run/backend/domain/crew/dto/response/CrewMonthlyCanlendarResponse.java index 763cb98..5dda127 100644 --- a/src/main/java/run/backend/domain/crew/dto/response/CrewMonthlyCanlendarResponse.java +++ b/src/main/java/run/backend/domain/crew/dto/response/CrewMonthlyCanlendarResponse.java @@ -1,9 +1,10 @@ package run.backend.domain.crew.dto.response; -import java.util.List; +import run.backend.domain.crew.dto.common.DayStatusDto; +import java.util.Map; public record CrewMonthlyCanlendarResponse( - List records, - List events + + Map monthlyRunningStatus ) { } diff --git a/src/main/java/run/backend/domain/crew/dto/response/CrewWeeklyEventResponse.java b/src/main/java/run/backend/domain/crew/dto/response/CrewWeeklyEventResponse.java new file mode 100644 index 0000000..960e0b7 --- /dev/null +++ b/src/main/java/run/backend/domain/crew/dto/response/CrewWeeklyEventResponse.java @@ -0,0 +1,13 @@ +package run.backend.domain.crew.dto.response; + +import run.backend.domain.crew.dto.common.DayStatusDto; + +import java.time.DayOfWeek; +import java.util.Map; + +public record CrewWeeklyEventResponse( + int currentDay, + Map weeklyRunningStatus + +) { +} diff --git a/src/main/java/run/backend/domain/crew/dto/response/EventProfileResponse.java b/src/main/java/run/backend/domain/crew/dto/response/EventProfileResponse.java index 042415c..1a8f594 100644 --- a/src/main/java/run/backend/domain/crew/dto/response/EventProfileResponse.java +++ b/src/main/java/run/backend/domain/crew/dto/response/EventProfileResponse.java @@ -1,4 +1,18 @@ package run.backend.domain.crew.dto.response; -public record EventProfileResponse() { +import lombok.Builder; + +import java.time.LocalDate; +import java.time.LocalTime; + +@Builder +public record EventProfileResponse( + + Long eventId, + String title, + LocalDate date, + LocalTime startTime, + LocalTime endTime, + Long participants +) { } diff --git a/src/main/java/run/backend/domain/event/enums/RunningStatus.java b/src/main/java/run/backend/domain/event/enums/RunningStatus.java new file mode 100644 index 0000000..e069ffa --- /dev/null +++ b/src/main/java/run/backend/domain/event/enums/RunningStatus.java @@ -0,0 +1,14 @@ +package run.backend.domain.event.enums; + +public enum RunningStatus { + + NONE("일정 없음"), + SCHEDULED("예정"), + DONE("완료"); + + private final String description; + + RunningStatus(String description) { + this.description = description; + } +} From 464870e172a1c55e397632e13bcab22f83138934 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 20 Jul 2025 16:39:52 +0900 Subject: [PATCH 27/40] =?UTF-8?q?[#20]=20feat:=20=EA=B8=B0=EA=B0=84?= =?UTF-8?q?=EB=B3=84=20=EC=9D=BC=EC=A0=95=20=EC=A1=B0=ED=9A=8C=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=BD=94=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 --- .../backend/domain/event/repository/EventRepository.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/run/backend/domain/event/repository/EventRepository.java b/src/main/java/run/backend/domain/event/repository/EventRepository.java index 94b76e6..69d8c6c 100644 --- a/src/main/java/run/backend/domain/event/repository/EventRepository.java +++ b/src/main/java/run/backend/domain/event/repository/EventRepository.java @@ -1,8 +1,16 @@ package run.backend.domain.event.repository; import org.springframework.data.jpa.repository.JpaRepository; +import run.backend.domain.crew.entity.Crew; import run.backend.domain.event.entity.Event; +import java.time.LocalDate; +import java.util.List; + public interface EventRepository extends JpaRepository { + List findAllByCrewAndDateBetween(Crew crew, LocalDate startOfWeek, LocalDate endOfWeek); + + List findAllByCrewAndDateAfter(Crew crew, LocalDate today); + } From 3f5848ec0f381189dd03258648a546f8556c4973 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 21 Jul 2025 00:01:07 +0900 Subject: [PATCH 28/40] =?UTF-8?q?[#20]=20delete:=20dto=20=EB=82=B4?= =?UTF-8?q?=EC=97=90=20of=20=ED=95=A8=EC=88=98=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/dto/response/CrewBaseInfoResponse.java | 14 -------------- .../crew/dto/response/CrewProfileResponse.java | 14 -------------- 2 files changed, 28 deletions(-) diff --git a/src/main/java/run/backend/domain/crew/dto/response/CrewBaseInfoResponse.java b/src/main/java/run/backend/domain/crew/dto/response/CrewBaseInfoResponse.java index b333bc7..099874e 100644 --- a/src/main/java/run/backend/domain/crew/dto/response/CrewBaseInfoResponse.java +++ b/src/main/java/run/backend/domain/crew/dto/response/CrewBaseInfoResponse.java @@ -1,7 +1,6 @@ package run.backend.domain.crew.dto.response; import lombok.Builder; -import run.backend.domain.crew.entity.Crew; import java.math.BigDecimal; @@ -17,17 +16,4 @@ public record CrewBaseInfoResponse( Long monthlyTimeTotal ) { - - public static CrewBaseInfoResponse of(int rank, Crew crew) { - - return CrewBaseInfoResponse.builder() - .rank(rank) - .image(crew.getImage()) - .name(crew.getName()) - .description(crew.getDescription()) - .memberCount(crew.getMemberCount()) - .monthlyDistanceTotal(crew.getMonthlyDistanceTotal()) - .monthlyTimeTotal(crew.getMonthlyTimeTotal()) - .build(); - } } diff --git a/src/main/java/run/backend/domain/crew/dto/response/CrewProfileResponse.java b/src/main/java/run/backend/domain/crew/dto/response/CrewProfileResponse.java index 629e9dc..b6a8a98 100644 --- a/src/main/java/run/backend/domain/crew/dto/response/CrewProfileResponse.java +++ b/src/main/java/run/backend/domain/crew/dto/response/CrewProfileResponse.java @@ -1,8 +1,6 @@ package run.backend.domain.crew.dto.response; import lombok.Builder; -import run.backend.domain.crew.entity.Crew; -import run.backend.domain.member.entity.Member; @Builder public record CrewProfileResponse( @@ -13,16 +11,4 @@ public record CrewProfileResponse( String leaderImage, String leaderName ) { - - public static CrewProfileResponse of(Crew crew, Member leader) { - - return new CrewProfileResponse( - crew.getImage(), - crew.getName(), - crew.getDescription(), - crew.getMemberCount(), - leader.getProfileImage(), - leader.getNickname() - ); - } } From 759ac96bce75425a43ab2ad28a355bf1be540072 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 21 Jul 2025 00:01:36 +0900 Subject: [PATCH 29/40] =?UTF-8?q?[#20]=20refact:=20crewServiceImpl=20->=20?= =?UTF-8?q?crewService=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/crew/service/CrewService.java | 119 +++++++++++++++--- .../domain/crew/service/CrewServiceImpl.java | 113 ----------------- 2 files changed, 99 insertions(+), 133 deletions(-) delete mode 100644 src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java diff --git a/src/main/java/run/backend/domain/crew/service/CrewService.java b/src/main/java/run/backend/domain/crew/service/CrewService.java index 04fbfe4..7339163 100644 --- a/src/main/java/run/backend/domain/crew/service/CrewService.java +++ b/src/main/java/run/backend/domain/crew/service/CrewService.java @@ -1,38 +1,117 @@ package run.backend.domain.crew.service; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import run.backend.domain.crew.dto.common.CrewInviteCodeDto; import run.backend.domain.crew.dto.request.CrewInfoRequest; import run.backend.domain.crew.dto.response.*; import run.backend.domain.crew.entity.Crew; +import run.backend.domain.crew.entity.JoinCrew; +import run.backend.domain.crew.enums.JoinStatus; +import run.backend.domain.crew.exception.CrewException; +import run.backend.domain.crew.mapper.CrewMapper; +import run.backend.domain.crew.repository.CrewRepository; +import run.backend.domain.crew.repository.JoinCrewRepository; +import run.backend.domain.file.service.FileService; import run.backend.domain.member.entity.Member; import run.backend.domain.member.enums.Role; +import run.backend.domain.member.repository.MemberRepository; -import java.time.YearMonth; +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CrewService { -public interface CrewService { + private final CrewMapper crewMapper; + private final FileService fileService; + private final CrewRepository crewRepository; + private final MemberRepository memberRepository; + private final JoinCrewRepository joinCrewRepository; - CrewInviteCodeDto createCrew(Member member, String imageStatus, MultipartFile image, CrewInfoRequest crewInfoRequest); + @Transactional + public CrewInviteCodeDto createCrew(Member member, String imageStatus, MultipartFile image, CrewInfoRequest data) { - void updateCrew(Member member, Crew crew, String imageStatus, MultipartFile image, CrewInfoRequest crewInfoRequest); + if (joinCrewRepository.existsByMemberAndJoinStatus(member, JoinStatus.APPROVED)) + throw new CrewException.AlreadyJoinedCrew(); - CrewInviteCodeDto getCrewInviteCode(Crew crew); + // 1. Crew 생성 + String imageName = "default-profile-image.png"; + if (imageStatus.equals("updated")) + imageName = fileService.saveProfileImage(image); + Crew crew = Crew.builder() + .image(imageName) + .name(data.name()) + .description(data.description()) + .build(); + crewRepository.save(crew); - CrewProfileResponse getCrewByInviteCode(String inviteCode); + // 2. JoinCrew 생성 + JoinCrew joinCrew = JoinCrew.createLeaderJoin(member, crew); + joinCrewRepository.save(joinCrew); - void joinCrew(Member member, Long crewId); + // 3. Member Role LEADER 로 변경 + member.updateRole(Role.LEADER); + memberRepository.save(member); -// CrewSearchResponse searchCrew(String crewName); -// -// CrewInfoResponse getCrewInfo(Long crewId); -// -// CrewMonthlyCanlendarResponse getCrewMonthlyCalendar(Long crewId, YearMonth yearMonth); -// -// CrewUpcomingEventResponse getUpcomingEvents(Long crewId); -// -// CrewMemberResponse getCrewMemberProfile(Long crewId); -// -// void updateCrewMemberRole(Long memberId, Role role); -// -// CrewSearchResponse getRankCrew(); + return new CrewInviteCodeDto(crew.getInviteCode()); + } + + @Transactional + public void updateCrew(Member member, Crew crew, String imageStatus, MultipartFile image, CrewInfoRequest data) { + + switch (imageStatus) { + + case "updated" : + fileService.deleteImage(crew.getImage()); + String newImageName = fileService.saveProfileImage(image); + crew.updateImage(newImageName); + break; + case "removed" : + fileService.deleteImage(crew.getImage()); + crew.updateImage("default-profile-image.png"); + break; + } + + if (data.name() != null) + crew.updateName(data.name()); + if (data.description() != null) + crew.updateDescription(data.description()); + + crewRepository.save(crew); + } + + public CrewInviteCodeDto getCrewInviteCode(Crew crew) { + + return new CrewInviteCodeDto(crew.getInviteCode()); + } + + public CrewProfileResponse getCrewByInviteCode(String inviteCode) { + + Crew crew = crewRepository.findByInviteCode(inviteCode) + .orElseThrow(CrewException.NotFoundCrew::new); + Member leader = joinCrewRepository.findCrewLeader(Role.LEADER, crew); + + return crewMapper.toCrewProfile(crew, leader); + } + + @Transactional + public void joinCrew(Member member, Long crewId) { + + Crew crew = crewRepository.findById(crewId) + .orElseThrow(CrewException.NotFoundCrew::new); + if (joinCrewRepository.existsByMemberAndJoinStatus(member, JoinStatus.APPROVED)) + throw new CrewException.AlreadyJoinedCrew(); + + JoinCrew joinCrew = JoinCrew.createAppliedJoin(member, crew); + joinCrewRepository.save(joinCrew); + } + + public CrewBaseInfoResponse getCrewBaseInfo(Crew crew) { + + int rank = 0; // [TODO] : 스케줄링 rank 계산 구현 수정 예정 + + return crewMapper.toCrewBaseInfo(rank, crew); + } } diff --git a/src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java b/src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java deleted file mode 100644 index 0693223..0000000 --- a/src/main/java/run/backend/domain/crew/service/CrewServiceImpl.java +++ /dev/null @@ -1,113 +0,0 @@ -package run.backend.domain.crew.service; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; -import run.backend.domain.crew.dto.common.CrewInviteCodeDto; -import run.backend.domain.crew.dto.request.CrewInfoRequest; -import run.backend.domain.crew.dto.response.CrewProfileResponse; -import run.backend.domain.crew.entity.Crew; -import run.backend.domain.crew.entity.JoinCrew; -import run.backend.domain.crew.enums.JoinStatus; -import run.backend.domain.crew.exception.CrewException; -import run.backend.domain.crew.repository.CrewRepository; -import run.backend.domain.crew.repository.JoinCrewRepository; -import run.backend.domain.file.service.FileService; -import run.backend.domain.member.entity.Member; -import run.backend.domain.member.enums.Role; -import run.backend.domain.member.repository.MemberRepository; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class CrewServiceImpl implements CrewService { - - private final FileService fileService; - private final CrewRepository crewRepository; - private final MemberRepository memberRepository; - private final JoinCrewRepository joinCrewRepository; - - @Override - @Transactional - public CrewInviteCodeDto createCrew(Member member, String imageStatus, MultipartFile image, CrewInfoRequest data) { - - if (joinCrewRepository.existsByMemberAndJoinStatus(member, JoinStatus.APPROVED)) - throw new CrewException.AlreadyJoinedCrew(); - - // 1. Crew 생성 - String imageName = "default-profile-image.png"; - if (imageStatus.equals("updated")) - imageName = fileService.saveProfileImage(image); - Crew crew = Crew.builder() - .image(imageName) - .name(data.name()) - .description(data.description()) - .build(); - crewRepository.save(crew); - - // 2. JoinCrew 생성 - JoinCrew joinCrew = JoinCrew.createLeaderJoin(member, crew); - joinCrewRepository.save(joinCrew); - - // 3. Member Role LEADER 로 변경 - member.updateRole(Role.LEADER); - memberRepository.save(member); - - return new CrewInviteCodeDto(crew.getInviteCode()); - } - - @Override - @Transactional - public void updateCrew(Member member, Crew crew, String imageStatus, MultipartFile image, CrewInfoRequest data) { - - switch (imageStatus) { - - case "updated" : - fileService.deleteImage(crew.getImage()); - String newImageName = fileService.saveProfileImage(image); - crew.updateImage(newImageName); - break; - case "removed" : - fileService.deleteImage(crew.getImage()); - crew.updateImage("default-profile-image.png"); - break; - } - - if (data.name() != null) - crew.updateName(data.name()); - if (data.description() != null) - crew.updateDescription(data.description()); - - crewRepository.save(crew); - } - - @Override - public CrewInviteCodeDto getCrewInviteCode(Crew crew) { - - return new CrewInviteCodeDto(crew.getInviteCode()); - } - - @Override - public CrewProfileResponse getCrewByInviteCode(String inviteCode) { - - Crew crew = crewRepository.findByInviteCode(inviteCode) - .orElseThrow(CrewException.NotFoundCrew::new); - Member leader = joinCrewRepository.findCrewLeader(Role.LEADER, crew); - - return CrewProfileResponse.of(crew, leader); - } - - @Override - @Transactional - public void joinCrew(Member member, Long crewId) { - - Crew crew = crewRepository.findById(crewId) - .orElseThrow(CrewException.NotFoundCrew::new); - if (joinCrewRepository.existsByMemberAndJoinStatus(member, JoinStatus.APPROVED)) - throw new CrewException.AlreadyJoinedCrew(); - - JoinCrew joinCrew = JoinCrew.createAppliedJoin(member, crew); - joinCrewRepository.save(joinCrew); - } -} From 65377d02d935e310a95699b44a5e8be36743df45 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 21 Jul 2025 00:01:43 +0900 Subject: [PATCH 30/40] =?UTF-8?q?[#20]=20refact:=20crewServiceImpl=20->=20?= =?UTF-8?q?crewService=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/run/backend/domain/crew/service/CrewServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/run/backend/domain/crew/service/CrewServiceTest.java b/src/test/java/run/backend/domain/crew/service/CrewServiceTest.java index b87a094..b44838f 100644 --- a/src/test/java/run/backend/domain/crew/service/CrewServiceTest.java +++ b/src/test/java/run/backend/domain/crew/service/CrewServiceTest.java @@ -34,7 +34,7 @@ public class CrewServiceTest { @InjectMocks - private CrewServiceImpl crewService; + private CrewService crewService; @Mock private FileService fileService; From 2537b7786cc641c30919a32ce106ce5b95cedc83 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 21 Jul 2025 00:02:02 +0900 Subject: [PATCH 31/40] =?UTF-8?q?[#20]=20feat:=20=ED=81=AC=EB=A3=A8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8A=94=20API?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/controller/CrewController.java | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/src/main/java/run/backend/domain/crew/controller/CrewController.java b/src/main/java/run/backend/domain/crew/controller/CrewController.java index 15b2205..c958507 100644 --- a/src/main/java/run/backend/domain/crew/controller/CrewController.java +++ b/src/main/java/run/backend/domain/crew/controller/CrewController.java @@ -8,9 +8,10 @@ import org.springframework.web.multipart.MultipartFile; import run.backend.domain.crew.dto.common.CrewInviteCodeDto; import run.backend.domain.crew.dto.request.CrewInfoRequest; -import run.backend.domain.crew.dto.response.CrewProfileResponse; +import run.backend.domain.crew.dto.response.*; import run.backend.domain.crew.entity.Crew; -import run.backend.domain.crew.service.CrewServiceImpl; +import run.backend.domain.crew.service.CrewEventService; +import run.backend.domain.crew.service.CrewService; import run.backend.domain.member.entity.Member; import run.backend.global.annotation.member.Login; import run.backend.global.annotation.member.MemberCrew; @@ -22,7 +23,8 @@ @Tag(name = "Crews", description = "Crew 관련 API") public class CrewController { - private final CrewServiceImpl crewService; + private final CrewService crewService; + private final CrewEventService crewEventService; @PostMapping @Operation(summary = "크루 생성", description = "크루 생성하는 API 입니다.") @@ -77,4 +79,46 @@ public CommonResponse joinCrew( crewService.joinCrew(member, crewId); return new CommonResponse<>("크루 가입 성공"); } + + @GetMapping + @Operation(summary = "크루 기본 정보 조회", description = "크루 기본 정보를 조회하는 API 입니다.") + public CommonResponse getCrewBaseInfo(@MemberCrew Crew crew) { + + CrewBaseInfoResponse response = crewService.getCrewBaseInfo(crew); + return new CommonResponse<>("크루 기본 정보 조회 성공", response); + } + + @GetMapping("/events/weekly") + @Operation(summary = "weekly 기록 조회", description = "크루의 weekly 기록 조회하는 API 입니다.") + public CommonResponse getWeeklyRecord(@MemberCrew Crew crew) { + + CrewWeeklyEventResponse response = crewEventService.getCrewWeeklyEvent(crew); + return new CommonResponse<>("크루 주간 기록 조회 성공", response); + } + + @GetMapping("/events/monthly") + @Operation(summary = "monthly 일정 조회", description = "크루의 monthly 일정 조회하는 API 입니다.") + public CommonResponse getMonthlyEvent( + @MemberCrew Crew crew, + @RequestParam int year, + @RequestParam int month) { + + CrewMonthlyCanlendarResponse response = crewEventService.getCrewMonthlyCalendar(crew, year, month); + return new CommonResponse<>("크루 월간 기록 조회 성공", response); + } + + @GetMapping("/events/upcoming") + @Operation(summary = "upcoming 일정 조회", description = "크루의 upcoming 일정 조회하는 API 입니다.") + public CommonResponse getUpcomingEvent(@MemberCrew Crew crew) { + + CrewUpcomingEventResponse response = crewEventService.getCrewUpcomingEvent(crew); + return new CommonResponse<>("크루 다가오는 일정 조회 성공", response); + } + + @GetMapping("/members") + @Operation(summary = "크루원 조회", description = "크루 내 모든 크루원을 조회하는 API 입니다.") + public CommonResponse getCrewMember(@MemberCrew Crew crew) { + + return new CommonResponse<>("크루원 조회 성공"); + } } From 16c732677d2cf068ce79c3b17341ecc10a132db3 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 21 Jul 2025 00:02:26 +0900 Subject: [PATCH 32/40] =?UTF-8?q?[#20]=20feat:=20crew,=20event=20=EB=A7=A4?= =?UTF-8?q?=ED=8D=BC=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/crew/mapper/CrewMapper.java | 34 +++++++++++ .../domain/event/mapper/EventMapper.java | 29 +++++++++ .../event/mapper/EventStatusMapper.java | 61 +++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 src/main/java/run/backend/domain/crew/mapper/CrewMapper.java create mode 100644 src/main/java/run/backend/domain/event/mapper/EventMapper.java create mode 100644 src/main/java/run/backend/domain/event/mapper/EventStatusMapper.java diff --git a/src/main/java/run/backend/domain/crew/mapper/CrewMapper.java b/src/main/java/run/backend/domain/crew/mapper/CrewMapper.java new file mode 100644 index 0000000..4bee3ec --- /dev/null +++ b/src/main/java/run/backend/domain/crew/mapper/CrewMapper.java @@ -0,0 +1,34 @@ +package run.backend.domain.crew.mapper; + +import org.springframework.stereotype.Component; +import run.backend.domain.crew.dto.response.CrewBaseInfoResponse; +import run.backend.domain.crew.dto.response.CrewProfileResponse; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.member.entity.Member; + +@Component +public class CrewMapper { + + public CrewProfileResponse toCrewProfile(Crew crew, Member leader) { + return CrewProfileResponse.builder() + .crewImage(crew.getImage()) + .crewName(crew.getName()) + .crewDescription(crew.getDescription()) + .memberCount(crew.getMemberCount()) + .leaderImage(leader.getProfileImage()) + .leaderName(leader.getNickname()) + .build(); + } + + public CrewBaseInfoResponse toCrewBaseInfo(int rank, Crew crew) { + return CrewBaseInfoResponse.builder() + .rank(rank) + .image(crew.getImage()) + .name(crew.getName()) + .description(crew.getDescription()) + .memberCount(crew.getMemberCount()) + .monthlyDistanceTotal(crew.getMonthlyDistanceTotal()) + .monthlyTimeTotal(crew.getMonthlyTimeTotal()) + .build(); + } +} diff --git a/src/main/java/run/backend/domain/event/mapper/EventMapper.java b/src/main/java/run/backend/domain/event/mapper/EventMapper.java new file mode 100644 index 0000000..53e84b2 --- /dev/null +++ b/src/main/java/run/backend/domain/event/mapper/EventMapper.java @@ -0,0 +1,29 @@ +package run.backend.domain.event.mapper; + +import org.springframework.stereotype.Component; +import run.backend.domain.crew.dto.response.EventProfileResponse; +import run.backend.domain.event.entity.Event; + +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class EventMapper { + + public List toEventProfileList(List events) { + return events.stream() + .map(this::toEventProfile) + .collect(Collectors.toList()); + } + + public EventProfileResponse toEventProfile(Event event) { + return EventProfileResponse.builder() + .eventId(event.getId()) + .title(event.getTitle()) + .date(event.getDate()) + .startTime(event.getStartTime()) + .endTime(event.getEndTime()) + .participants(event.getExpectedParticipants()) + .build(); + } +} diff --git a/src/main/java/run/backend/domain/event/mapper/EventStatusMapper.java b/src/main/java/run/backend/domain/event/mapper/EventStatusMapper.java new file mode 100644 index 0000000..3f083cf --- /dev/null +++ b/src/main/java/run/backend/domain/event/mapper/EventStatusMapper.java @@ -0,0 +1,61 @@ +package run.backend.domain.event.mapper; + +import org.springframework.stereotype.Component; +import run.backend.domain.crew.dto.common.DayStatusDto; +import run.backend.domain.event.entity.Event; +import run.backend.domain.event.enums.RunningStatus; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +public class EventStatusMapper { + + public Map toWeeklyStatus(List events, LocalDate today) { + + // MON ~ SUN 초기화 + Map statusMap = new EnumMap<>(DayOfWeek.class); + for (DayOfWeek day : DayOfWeek.values()) { + statusMap.put(day, new DayStatusDto(RunningStatus.NONE, null)); + } + + // Running Status 바꿔주기 + for (Event event : events) { + + LocalDate eventDate = event.getDate(); + DayOfWeek day = eventDate.getDayOfWeek(); + + RunningStatus status = eventDate.isBefore(today) + ? RunningStatus.DONE + : RunningStatus.SCHEDULED; + statusMap.put(day, new DayStatusDto(status, event.getId())); + } + return statusMap; + } + + public Map toMonthlyStatus(List events, LocalDate today, int endDate) { + + // 1~endDate까지 초기화 + Map statusMap = new HashMap<>(); + for (int i = 1; i <= endDate; i++) { + statusMap.put(i, new DayStatusDto(RunningStatus.NONE, null)); + } + + // Running Status 바꿔주기 + for (Event event : events) { + + LocalDate eventDate = event.getDate(); + int day = eventDate.getDayOfMonth(); + + RunningStatus status = eventDate.isBefore(today) + ? RunningStatus.DONE + : RunningStatus.SCHEDULED; + statusMap.put(day, new DayStatusDto(status, event.getId())); + } + return statusMap; + } +} From c6046b71b592ad3a0dbaa3c31edf28393fed1976 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 21 Jul 2025 00:02:43 +0900 Subject: [PATCH 33/40] =?UTF-8?q?[#20]=20feat:=20date=20range=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=ED=95=98=EB=8A=94=20=EC=9C=A0=ED=8B=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../run/backend/global/dto/DateRange.java | 9 ++++++++ .../backend/global/util/DateRangeUtil.java | 23 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/main/java/run/backend/global/dto/DateRange.java create mode 100644 src/main/java/run/backend/global/util/DateRangeUtil.java diff --git a/src/main/java/run/backend/global/dto/DateRange.java b/src/main/java/run/backend/global/dto/DateRange.java new file mode 100644 index 0000000..a21359d --- /dev/null +++ b/src/main/java/run/backend/global/dto/DateRange.java @@ -0,0 +1,9 @@ +package run.backend.global.dto; + +import java.time.LocalDate; + +public record DateRange( + LocalDate start, + LocalDate end +) { +} diff --git a/src/main/java/run/backend/global/util/DateRangeUtil.java b/src/main/java/run/backend/global/util/DateRangeUtil.java new file mode 100644 index 0000000..c58eefb --- /dev/null +++ b/src/main/java/run/backend/global/util/DateRangeUtil.java @@ -0,0 +1,23 @@ +package run.backend.global.util; + +import org.springframework.stereotype.Component; +import run.backend.global.dto.DateRange; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.YearMonth; + +@Component +public class DateRangeUtil { + + public DateRange getWeekRange(LocalDate today) { + + return new DateRange(today.with(DayOfWeek.MONDAY), today.with(DayOfWeek.SUNDAY)); + } + + public DateRange getMonthRange(int year, int month) { + + YearMonth ym = YearMonth.of(year, month); + return new DateRange(ym.atDay(1), ym.atEndOfMonth()); + } +} From 82924d95a0e282bb551d670dab993c266ccc688f Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 21 Jul 2025 00:03:05 +0900 Subject: [PATCH 34/40] =?UTF-8?q?[#20]=20feat:=20week,=20month,=20upcoming?= =?UTF-8?q?=20=EC=9D=BC=EC=A0=95=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/crew/service/CrewEventService.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/main/java/run/backend/domain/crew/service/CrewEventService.java diff --git a/src/main/java/run/backend/domain/crew/service/CrewEventService.java b/src/main/java/run/backend/domain/crew/service/CrewEventService.java new file mode 100644 index 0000000..1311638 --- /dev/null +++ b/src/main/java/run/backend/domain/crew/service/CrewEventService.java @@ -0,0 +1,64 @@ +package run.backend.domain.crew.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import run.backend.domain.crew.dto.common.DayStatusDto; +import run.backend.domain.crew.dto.response.CrewMonthlyCanlendarResponse; +import run.backend.domain.crew.dto.response.CrewUpcomingEventResponse; +import run.backend.domain.crew.dto.response.CrewWeeklyEventResponse; +import run.backend.domain.crew.dto.response.EventProfileResponse; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.event.mapper.EventMapper; +import run.backend.domain.event.mapper.EventStatusMapper; +import run.backend.domain.event.entity.Event; +import run.backend.domain.event.repository.EventRepository; +import run.backend.global.dto.DateRange; +import run.backend.global.util.DateRangeUtil; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.*; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CrewEventService { + + private final DateRangeUtil dateRangeUtil; + private final EventRepository eventRepository; + private final EventStatusMapper eventStatusMapper; + private final EventMapper eventMapper; + + public CrewWeeklyEventResponse getCrewWeeklyEvent(Crew crew) { + + LocalDate today = LocalDate.now(); + DateRange dateRange = dateRangeUtil.getWeekRange(today); + + List weeklyEvents = eventRepository.findAllByCrewAndDateBetween(crew, dateRange.start(), dateRange.end()); + Map statusMap = eventStatusMapper.toWeeklyStatus(weeklyEvents, today); + + return new CrewWeeklyEventResponse(today.getDayOfWeek().getValue(), statusMap); + } + + public CrewMonthlyCanlendarResponse getCrewMonthlyCalendar(Crew crew, int year, int month) { + + LocalDate today = LocalDate.now(); + DateRange dateRange = dateRangeUtil.getMonthRange(year, month); + + List events = eventRepository.findAllByCrewAndDateBetween(crew, dateRange.start(), dateRange.end()); + Map statusMap = eventStatusMapper.toMonthlyStatus(events, today, dateRange.end().getDayOfMonth()); + + return new CrewMonthlyCanlendarResponse(statusMap); + } + + public CrewUpcomingEventResponse getCrewUpcomingEvent(Crew crew) { + + LocalDate today = LocalDate.now(); + + List events = eventRepository.findAllByCrewAndDateAfter(crew, today); + List eventProfiles = eventMapper.toEventProfileList(events); + + return new CrewUpcomingEventResponse(eventProfiles); + } +} From 356e21ce2b43db33e3dd04c3cce71fb81cbd58b8 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 21 Jul 2025 00:57:25 +0900 Subject: [PATCH 35/40] =?UTF-8?q?[#20]=20test:=20CrewEventService=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/service/CrewEventServiceTest.java | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/test/java/run/backend/domain/crew/service/CrewEventServiceTest.java diff --git a/src/test/java/run/backend/domain/crew/service/CrewEventServiceTest.java b/src/test/java/run/backend/domain/crew/service/CrewEventServiceTest.java new file mode 100644 index 0000000..e230ba0 --- /dev/null +++ b/src/test/java/run/backend/domain/crew/service/CrewEventServiceTest.java @@ -0,0 +1,142 @@ +package run.backend.domain.crew.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import run.backend.domain.crew.dto.common.DayStatusDto; +import run.backend.domain.crew.dto.response.CrewMonthlyCanlendarResponse; +import run.backend.domain.crew.dto.response.CrewUpcomingEventResponse; +import run.backend.domain.crew.dto.response.CrewWeeklyEventResponse; +import run.backend.domain.crew.dto.response.EventProfileResponse; +import run.backend.domain.crew.entity.Crew; +import run.backend.domain.event.entity.Event; +import run.backend.domain.event.enums.RunningStatus; +import run.backend.domain.event.mapper.EventMapper; +import run.backend.domain.event.mapper.EventStatusMapper; +import run.backend.domain.event.repository.EventRepository; +import run.backend.global.dto.DateRange; +import run.backend.global.util.DateRangeUtil; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.as; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@DisplayName("CrewEvent 서비스 테스트") +@ExtendWith(MockitoExtension.class) +public class CrewEventServiceTest { + + @InjectMocks + private CrewEventService crewEventService; + + @Mock + private DateRangeUtil dateRangeUtil; + + @Mock + private EventRepository eventRepository; + + @Mock + private EventStatusMapper eventStatusMapper; + + @Mock + private EventMapper eventMapper; + + @Mock + private Crew crew; + + @Test + @DisplayName("이번주 일정을 정리해서 반환") + void shouldReturnWeeklyEvent() { + + // given + LocalDate startOfWeek = LocalDate.of(2025, 7, 14); // 월요일 + LocalDate endOfWeek = LocalDate.of(2025, 7, 20); // 일요일 + DateRange weekRange = new DateRange(startOfWeek, endOfWeek); + + List events = List.of(mock(Event.class)); + Map statusMap = Map.of( + DayOfWeek.MONDAY, new DayStatusDto(RunningStatus.SCHEDULED, 1L) + ); + + when(dateRangeUtil.getWeekRange(any(LocalDate.class))).thenReturn(weekRange); + when(eventRepository.findAllByCrewAndDateBetween(crew, startOfWeek, endOfWeek)).thenReturn(events); + when(eventStatusMapper.toWeeklyStatus(eq(events), any(LocalDate.class))).thenReturn(statusMap); + + // when + CrewWeeklyEventResponse response = crewEventService.getCrewWeeklyEvent(crew); + + // then + assertThat(response.currentDay()).isBetween(1, 7); + assertThat(response.weeklyRunningStatus()).isEqualTo(statusMap); + + // verify + verify(dateRangeUtil).getWeekRange(any(LocalDate.class)); + verify(eventRepository).findAllByCrewAndDateBetween(crew, startOfWeek, endOfWeek); + verify(eventStatusMapper).toWeeklyStatus(eq(events), any(LocalDate.class)); + + } + + @Test + @DisplayName("이번달 일정을 정리해서 반환") + void shouldReturnMonthlyEvent() { + + // given + int year = 2025; + int month = 7; + LocalDate start = LocalDate.of(2025, 7, 1); + LocalDate end = LocalDate.of(2025, 7, 31); + DateRange dateRange = new DateRange(start, end); + + List events = List.of(mock(Event.class)); + Map statusMap = Map.of( + 5, new DayStatusDto(RunningStatus.SCHEDULED, 1L) + ); + + when(dateRangeUtil.getMonthRange(year, month)).thenReturn(dateRange); + when(eventRepository.findAllByCrewAndDateBetween(crew, start, end)).thenReturn(events); + when(eventStatusMapper.toMonthlyStatus(eq(events), any(LocalDate.class), eq(31))).thenReturn(statusMap); + + // when + CrewMonthlyCanlendarResponse response = crewEventService.getCrewMonthlyCalendar(crew, year, month); + + // then + assertThat(response.monthlyRunningStatus()).isEqualTo(statusMap); + + // verify + verify(dateRangeUtil).getMonthRange(year, month); + verify(eventRepository).findAllByCrewAndDateBetween(crew, start, end); + verify(eventStatusMapper).toMonthlyStatus(eq(events), any(LocalDate.class), eq(31)); + } + + @Test + @DisplayName("다가오는 일정을 정리해서 반환") + void shouldReturnUpcomingEvent() { + + // given + List events = List.of(mock(Event.class)); + List eventProfiles = List.of( + new EventProfileResponse(1L, "달리기", null, null, null, 1L) + ); + + when(eventRepository.findAllByCrewAndDateAfter(eq(crew), any(LocalDate.class))).thenReturn(events); + when(eventMapper.toEventProfileList(events)).thenReturn(eventProfiles); + + // when + CrewUpcomingEventResponse response = crewEventService.getCrewUpcomingEvent(crew); + + // then + assertThat(response.eventProfiles()).isEqualTo(eventProfiles); + + // verify + verify(eventRepository).findAllByCrewAndDateAfter(eq(crew), any(LocalDate.class)); + verify(eventMapper).toEventProfileList(events); + } + +} From 851cab0d608c3fecf5d9af3154c9d774174c74fd Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 21 Jul 2025 01:49:54 +0900 Subject: [PATCH 36/40] =?UTF-8?q?[#20]=20delete:=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=EC=97=86=EB=8A=94=20import=EB=AC=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../run/backend/domain/crew/service/CrewEventServiceTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/run/backend/domain/crew/service/CrewEventServiceTest.java b/src/test/java/run/backend/domain/crew/service/CrewEventServiceTest.java index e230ba0..5139bd5 100644 --- a/src/test/java/run/backend/domain/crew/service/CrewEventServiceTest.java +++ b/src/test/java/run/backend/domain/crew/service/CrewEventServiceTest.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Map; -import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; From 8bcd1f36fbf587d66d93f39af615c8e0c7b3bb0d Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 21 Jul 2025 01:50:10 +0900 Subject: [PATCH 37/40] =?UTF-8?q?[#20]=20test:=20EventStatusMapper=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/mapper/EventStatusMapperTest.java | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 src/test/java/run/backend/domain/event/mapper/EventStatusMapperTest.java diff --git a/src/test/java/run/backend/domain/event/mapper/EventStatusMapperTest.java b/src/test/java/run/backend/domain/event/mapper/EventStatusMapperTest.java new file mode 100644 index 0000000..2cef774 --- /dev/null +++ b/src/test/java/run/backend/domain/event/mapper/EventStatusMapperTest.java @@ -0,0 +1,166 @@ +package run.backend.domain.event.mapper; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import run.backend.domain.crew.dto.common.DayStatusDto; +import run.backend.domain.event.entity.Event; +import run.backend.domain.event.enums.RunningStatus; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Event Status Mapper 테스트") +@ExtendWith(MockitoExtension.class) +public class EventStatusMapperTest { + + @InjectMocks + private EventStatusMapper eventStatusMapper; + + private Event pastEvent; + + private Event futureEvent; + + private final LocalDate today = LocalDate.of(2025, 7, 18); // 금요일 (5) + + @BeforeEach + public void setup() { + + pastEvent = Event.builder() + .date(LocalDate.of(2025, 7, 17)) // 목요일 (4) + .build(); + + futureEvent = Event.builder() + .date(LocalDate.of(2025, 7, 19)) // 토요일 (6) + .build(); + } + + @Nested + @DisplayName("toWeeklyStatus 메서드는") + class toWeeklyStatusTest { + + @Test + @DisplayName("요일별 상태가 7개 모두 반환된다") + void returnsSevenDayStatusEntriesForFullWeek() { + + // given + List events = new ArrayList<>(); + + // when + Map response = eventStatusMapper.toWeeklyStatus(events, today); + + // then + assertThat(response.size()).isEqualTo(7); + } + + @Test + @DisplayName("이벤트가 없으면 NONE") + void shouldBeNone_whenEventEmpty() { + + // given + List events = new ArrayList<>(); + + // when + Map response = eventStatusMapper.toWeeklyStatus(events, today); + + // then + assertThat(response.size()).isEqualTo(7); + + for (DayStatusDto dto : response.values()) { + assertThat(dto.status()).isEqualTo(RunningStatus.NONE); + assertThat(dto.eventId()).isNull(); + } + + } + + @Test + @DisplayName("과거 이벤트이면 DONE") + void shouldBeDone_whenPastEvent() { + + // when + Map response = eventStatusMapper.toWeeklyStatus(List.of(pastEvent), today); + + // then + assertThat(response.size()).isEqualTo(7); + + DayOfWeek dayOfPastEvent = pastEvent.getDate().getDayOfWeek(); + DayStatusDto statusDto = response.get(dayOfPastEvent); + assertThat(statusDto.status()).isEqualTo(RunningStatus.DONE); + } + + @Test + @DisplayName("미래 이벤트이면 SCHEDULED") + void shouldBeSCHEDULED_whenFutureEvent() { + + // when + Map response = eventStatusMapper.toWeeklyStatus(List.of(futureEvent), today); + + // then + assertThat(response.size()).isEqualTo(7); + + DayOfWeek dayOfPastEvent = futureEvent.getDate().getDayOfWeek(); + DayStatusDto statusDto = response.get(dayOfPastEvent); + assertThat(statusDto.status()).isEqualTo(RunningStatus.SCHEDULED); + } + } + + @Nested + @DisplayName("toMonthlyStatus 메서드는") + class toMonthlyStatusTest { + + private final LocalDate today = LocalDate.of(2025, 7, 18); // 18일 (금요일) + private final int endDate = 31; + + @Test + @DisplayName("이벤트가 없으면 모든 날짜가 NONE") + void shouldBeNone_whenNoEvent() { + // given + List events = new ArrayList<>(); + + // when + Map result = eventStatusMapper.toMonthlyStatus(events, today, endDate); + + // then + assertThat(result).hasSize(endDate); + for (int i = 1; i <= endDate; i++) { + DayStatusDto status = result.get(i); + assertThat(status.status()).isEqualTo(RunningStatus.NONE); + assertThat(status.eventId()).isNull(); + } + } + + @Test + @DisplayName("과거 이벤트가 있으면 DONE 상태가 된다") + void shouldBeDone_whenEventInPast() { + + // when + Map result = eventStatusMapper.toMonthlyStatus(List.of(pastEvent), today, endDate); + + // then + DayStatusDto status = result.get(17); + assertThat(status.status()).isEqualTo(RunningStatus.DONE); + } + + @Test + @DisplayName("미래 이벤트가 있으면 SCHEDULED 상태가 된다") + void shouldBeScheduled_whenEventInFuture() { + + // when + Map result = eventStatusMapper.toMonthlyStatus(List.of(futureEvent), today, endDate); + + // then + DayStatusDto status = result.get(19); + assertThat(status.status()).isEqualTo(RunningStatus.SCHEDULED); + } + } +} From a5de59a0367fb544c46bc934fe292be42664bae4 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 21 Jul 2025 02:30:18 +0900 Subject: [PATCH 38/40] =?UTF-8?q?[#20]=20refact:=20fileService=EB=A1=9C=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crew/controller/CrewController.java | 3 +- .../domain/crew/service/CrewService.java | 39 +++++-------------- .../domain/file/service/FileService.java | 17 ++++++++ 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/src/main/java/run/backend/domain/crew/controller/CrewController.java b/src/main/java/run/backend/domain/crew/controller/CrewController.java index c958507..0aa0d01 100644 --- a/src/main/java/run/backend/domain/crew/controller/CrewController.java +++ b/src/main/java/run/backend/domain/crew/controller/CrewController.java @@ -43,14 +43,13 @@ public CommonResponse createCrew( @PreAuthorize("hasRole('MANAGER') or hasRole('LEADER')") @Operation(summary = "크루 정보 수정", description = "크루 정보 수정하는 API 입니다.") public CommonResponse updateCrewInfo( - @Login Member member, @MemberCrew Crew crew, @RequestParam String imageStatus, @RequestPart(value = "data")CrewInfoRequest data, @RequestPart(value = "image", required = false) MultipartFile image ) { - crewService.updateCrew(member, crew, imageStatus, image, data); + crewService.updateCrew(crew, imageStatus, image, data); return new CommonResponse<>("크루 정보 수정 성공"); } diff --git a/src/main/java/run/backend/domain/crew/service/CrewService.java b/src/main/java/run/backend/domain/crew/service/CrewService.java index 7339163..8987602 100644 --- a/src/main/java/run/backend/domain/crew/service/CrewService.java +++ b/src/main/java/run/backend/domain/crew/service/CrewService.java @@ -36,22 +36,13 @@ public CrewInviteCodeDto createCrew(Member member, String imageStatus, Multipart if (joinCrewRepository.existsByMemberAndJoinStatus(member, JoinStatus.APPROVED)) throw new CrewException.AlreadyJoinedCrew(); - // 1. Crew 생성 - String imageName = "default-profile-image.png"; - if (imageStatus.equals("updated")) - imageName = fileService.saveProfileImage(image); - Crew crew = Crew.builder() - .image(imageName) - .name(data.name()) - .description(data.description()) - .build(); + String imageName = fileService.handleImageUpdate(imageStatus, "default-profile-image.png", image); + Crew crew = crewMapper.toEntity(imageName, data.name(), data.description()); crewRepository.save(crew); - // 2. JoinCrew 생성 JoinCrew joinCrew = JoinCrew.createLeaderJoin(member, crew); joinCrewRepository.save(joinCrew); - // 3. Member Role LEADER 로 변경 member.updateRole(Role.LEADER); memberRepository.save(member); @@ -59,25 +50,13 @@ public CrewInviteCodeDto createCrew(Member member, String imageStatus, Multipart } @Transactional - public void updateCrew(Member member, Crew crew, String imageStatus, MultipartFile image, CrewInfoRequest data) { - - switch (imageStatus) { - - case "updated" : - fileService.deleteImage(crew.getImage()); - String newImageName = fileService.saveProfileImage(image); - crew.updateImage(newImageName); - break; - case "removed" : - fileService.deleteImage(crew.getImage()); - crew.updateImage("default-profile-image.png"); - break; - } - - if (data.name() != null) - crew.updateName(data.name()); - if (data.description() != null) - crew.updateDescription(data.description()); + public void updateCrew(Crew crew, String imageStatus, MultipartFile image, CrewInfoRequest data) { + + String imageName = fileService.handleImageUpdate(imageStatus, crew.getImage(), image); + crew.updateImage(imageName); + + if (data.name() != null) crew.updateName(data.name()); + if (data.description() != null) crew.updateDescription(data.description()); crewRepository.save(crew); } diff --git a/src/main/java/run/backend/domain/file/service/FileService.java b/src/main/java/run/backend/domain/file/service/FileService.java index db7615b..c91988a 100644 --- a/src/main/java/run/backend/domain/file/service/FileService.java +++ b/src/main/java/run/backend/domain/file/service/FileService.java @@ -29,6 +29,23 @@ public class FileService { "gif"); private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB + public String handleImageUpdate(String imageStatus, String currentImage, MultipartFile image) { + + switch (imageStatus) { + + case "updated": + deleteImage(currentImage); + return saveProfileImage(image); + + case "removed": + deleteImage(currentImage); + return "default-profile-image.png"; + + default: + return currentImage; + } + } + public void deleteImage(String fileName) { validateFilename(fileName); From 206aab61d66853635829b116bf0ab380fa976a7e Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 21 Jul 2025 02:30:37 +0900 Subject: [PATCH 39/40] =?UTF-8?q?[#20]=20feat:=20CrewMapper=EC=97=90=20toE?= =?UTF-8?q?ntity=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/run/backend/domain/crew/mapper/CrewMapper.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/run/backend/domain/crew/mapper/CrewMapper.java b/src/main/java/run/backend/domain/crew/mapper/CrewMapper.java index 4bee3ec..d58b5d2 100644 --- a/src/main/java/run/backend/domain/crew/mapper/CrewMapper.java +++ b/src/main/java/run/backend/domain/crew/mapper/CrewMapper.java @@ -9,6 +9,14 @@ @Component public class CrewMapper { + public Crew toEntity(String imageName, String name, String description) { + return Crew.builder() + .image(imageName) + .name(name) + .description(description) + .build(); + } + public CrewProfileResponse toCrewProfile(Crew crew, Member leader) { return CrewProfileResponse.builder() .crewImage(crew.getImage()) From 664d23cc8aafd61af40a22f5876dd55c28749fcc Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 21 Jul 2025 02:30:51 +0900 Subject: [PATCH 40/40] =?UTF-8?q?[#20]=20test:=20CrewServiceTest=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/crew/service/CrewServiceTest.java | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/src/test/java/run/backend/domain/crew/service/CrewServiceTest.java b/src/test/java/run/backend/domain/crew/service/CrewServiceTest.java index b44838f..84e1f13 100644 --- a/src/test/java/run/backend/domain/crew/service/CrewServiceTest.java +++ b/src/test/java/run/backend/domain/crew/service/CrewServiceTest.java @@ -15,6 +15,7 @@ import run.backend.domain.crew.entity.JoinCrew; import run.backend.domain.crew.enums.JoinStatus; import run.backend.domain.crew.exception.CrewException; +import run.backend.domain.crew.mapper.CrewMapper; import run.backend.domain.crew.repository.CrewRepository; import run.backend.domain.crew.repository.JoinCrewRepository; import run.backend.domain.file.service.FileService; @@ -49,6 +50,8 @@ public class CrewServiceTest { private MemberRepository memberRepository; @Mock + private CrewMapper crewMapper; + private Crew crew; private Member member; @@ -58,6 +61,11 @@ public class CrewServiceTest { void setUp() { member = Member.builder().username("테스트 유저").build(); request = new CrewInfoRequest("러너스", "러너스 크루입니다."); + crew = Crew.builder() + .name("테스트 크루") + .description("테스트 설명") + .image("default-profile-image.png") + .build(); } @Nested @@ -85,6 +93,10 @@ void respondsWithInviteCode_whenCreatingCrew() { // given when(joinCrewRepository.existsByMemberAndJoinStatus(member, JoinStatus.APPROVED)) .thenReturn(false); + when(fileService.handleImageUpdate(eq("unchanged"), eq("default-profile-image.png"), isNull())) + .thenReturn("default-profile-image.png"); + when(crewMapper.toEntity(eq("default-profile-image.png"), eq(request.name()), eq(request.description()))) + .thenReturn(crew); when(crewRepository.save(any(Crew.class))).thenAnswer(invocation -> invocation.getArgument(0)); when(joinCrewRepository.save(any(JoinCrew.class))).thenReturn(null); when(memberRepository.save(any())).thenReturn(member); @@ -102,6 +114,10 @@ void saveJoinCrew_whenCreatingCrew() { // given when(joinCrewRepository.existsByMemberAndJoinStatus(member, JoinStatus.APPROVED)).thenReturn(false); + when(fileService.handleImageUpdate(eq("unchanged"), eq("default-profile-image.png"), isNull())) + .thenReturn("default-profile-image.png"); + when(crewMapper.toEntity(eq("default-profile-image.png"), eq(request.name()), eq(request.description()))) + .thenReturn(crew); when(crewRepository.save(any(Crew.class))).thenAnswer(invocation -> invocation.getArgument(0)); when(joinCrewRepository.save(any(JoinCrew.class))).thenReturn(null); when(memberRepository.save(any(Member.class))).thenReturn(member); @@ -119,6 +135,10 @@ void updatesMemberRoleToLeader_whenCreatingCrew() { // given when(joinCrewRepository.existsByMemberAndJoinStatus(member, JoinStatus.APPROVED)).thenReturn(false); + when(fileService.handleImageUpdate(eq("unchanged"), eq("default-profile-image.png"), isNull())) + .thenReturn("default-profile-image.png"); + when(crewMapper.toEntity(eq("default-profile-image.png"), eq(request.name()), eq(request.description()))) + .thenReturn(crew); when(crewRepository.save(any(Crew.class))).thenAnswer(invocation -> invocation.getArgument(0)); when(joinCrewRepository.save(any(JoinCrew.class))).thenReturn(null); when(memberRepository.save(any(Member.class))).thenReturn(member); @@ -142,16 +162,18 @@ void updateImage_whenImageStatusIsUpdated() { // given String oldImageName = "old_image.png"; String newImageName = "new_image.png"; + MultipartFile mockFile = mock(MultipartFile.class); + Crew crew = mock(Crew.class); - when(crew.getImage()).thenReturn(oldImageName); // when 안에 Mock 객체 - when(fileService.saveProfileImage(any())).thenReturn(newImageName); + when(crew.getImage()).thenReturn(oldImageName); + when(fileService.handleImageUpdate(eq("updated"), eq(oldImageName), eq(mockFile))) + .thenReturn(newImageName); // when - crewService.updateCrew(member, crew, "updated", mock(MultipartFile.class), request); + crewService.updateCrew(crew, "updated", mockFile, request); // then - verify(fileService).deleteImage(oldImageName); - verify(fileService).saveProfileImage(any()); + verify(fileService).handleImageUpdate(eq("updated"), eq(oldImageName), eq(mockFile)); verify(crew).updateImage(newImageName); verify(crewRepository).save(crew); } @@ -162,13 +184,18 @@ void removeImage_whenImageStatusIsRemoved() { // given String oldImageName = "old_image.png"; + MultipartFile mockFile = mock(MultipartFile.class); + Crew crew = mock(Crew.class); + when(crew.getImage()).thenReturn(oldImageName); + when(fileService.handleImageUpdate(eq("removed"), eq(oldImageName), eq(mockFile))) + .thenReturn("default-profile-image.png"); // when - crewService.updateCrew(member, crew, "removed", mock(MultipartFile.class), request); + crewService.updateCrew(crew, "removed", mockFile, request); // then - verify(fileService).deleteImage(oldImageName); + verify(fileService).handleImageUpdate(eq("removed"), eq(oldImageName), eq(mockFile)); verify(crew).updateImage("default-profile-image.png"); verify(crewRepository).save(crew); } @@ -177,8 +204,11 @@ void removeImage_whenImageStatusIsRemoved() { @DisplayName("name이 null이 아니면 이름을 업데이트한다.") void updateName_whenNameIsNotNull() { + // given + Crew crew = mock(Crew.class); + // when - crewService.updateCrew(member, crew, "unchanged", null, request); + crewService.updateCrew(crew, "unchanged", null, request); // then verify(crew).updateName("러너스"); @@ -190,8 +220,11 @@ void updateName_whenNameIsNotNull() { @DisplayName("description null이 아니면 설명을 업데이트한다.") void updateDescription_whenDescriptionIsNotNull() { + // given + Crew crew = mock(Crew.class); + // when - crewService.updateCrew(member, crew, "unchanged", null, request); + crewService.updateCrew(crew, "unchanged", null, request); // then verify(crew).updateDescription("러너스 크루입니다.");