diff --git a/README.md b/README.md index 1425d3bf..b7b1a869 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # BangFer-Back_End 방구석 퍼거슨 벡엔드 리포지토리 +![image](https://github.com/user-attachments/assets/809dbaeb-b0b6-4eaf-ab32-6caaa95e48a0) + + ## 👥 Server 팀원 |고민영|김종우|김근식| |:-:|:-:|:-:| @@ -18,3 +21,6 @@ |:rocket: | `:rocket:` | `chore` | 패키지 매니저 수정 (Dockerfile, gradle, sh, yml) | |:fire: | `:fire:` | `delete` | 코드/파일 삭제 | |:ambulance: | `:ambulance:` | `!hotfix` | 급하게 치명적인 버그를 고쳐야 하는 경우 | + +## ERD +![image](https://github.com/user-attachments/assets/8966793b-2bdb-4cc0-b363-231ff155c102) diff --git a/src/main/java/com/capstone/BnagFer/domain/accounts/controller/AccountsController.java b/src/main/java/com/capstone/BnagFer/domain/accounts/controller/AccountsController.java index 168d081b..712be348 100644 --- a/src/main/java/com/capstone/BnagFer/domain/accounts/controller/AccountsController.java +++ b/src/main/java/com/capstone/BnagFer/domain/accounts/controller/AccountsController.java @@ -9,6 +9,7 @@ import com.capstone.BnagFer.domain.accounts.jwt.dto.JwtDto; import com.capstone.BnagFer.domain.accounts.jwt.exception.SecurityCustomException; import com.capstone.BnagFer.domain.accounts.jwt.exception.TokenErrorCode; +import com.capstone.BnagFer.domain.accounts.service.account.AccountsCommonService; import com.capstone.BnagFer.domain.accounts.service.account.AccountsQueryService; import com.capstone.BnagFer.domain.accounts.service.account.AccountsService; import com.capstone.BnagFer.domain.accounts.service.account.KakaoService; @@ -37,6 +38,7 @@ public class AccountsController { private final JwtProvider jwtProvider; private final KakaoService kakaoService; private final EmailService emailService; + private final AccountsCommonService accountsCommonService; @Operation(summary = "일반 로그인", description = "이메일, 비밀번호를 입력받아 로그인을 진행합니다. 이때, FCM토큰을 같이 넘겨줘야 함." + "반환 값으로 JWT accessToken과 refreshToken이 발급됨. accessToken 값을 Authorize에 인증") @@ -152,9 +154,16 @@ public ApiResponse verifyCode(@Valid @RequestBody EmailVerifyDto request boolean check = emailService.verifyCode(requestDto); if (check) { return ApiResponse.onSuccess("인증 완료!"); - } - else { + } else { return ApiResponse.onFailure(HttpStatus.BAD_REQUEST.name(), "인증 실패"); } } + + @Operation(summary = "이메일 사용 가능 여부 확인", description = "이메일을 입력받아 사용 가능한지 여부를 확인합니다.") + @GetMapping("/checkEmail") + public ApiResponse checkEmailAvailability(@RequestParam String email) { + accountsCommonService.checkUserEmail(email); + return ApiResponse.onSuccess("사용 가능한 이메일입니다."); + } + } diff --git a/src/main/java/com/capstone/BnagFer/domain/accounts/service/account/AccountsCommonService.java b/src/main/java/com/capstone/BnagFer/domain/accounts/service/account/AccountsCommonService.java index 87f7599c..cc0999de 100644 --- a/src/main/java/com/capstone/BnagFer/domain/accounts/service/account/AccountsCommonService.java +++ b/src/main/java/com/capstone/BnagFer/domain/accounts/service/account/AccountsCommonService.java @@ -2,6 +2,7 @@ import com.capstone.BnagFer.domain.accounts.entity.User; import com.capstone.BnagFer.domain.accounts.exception.AccountsExceptionHandler; +import com.capstone.BnagFer.domain.accounts.repository.UserJpaRepository; import com.capstone.BnagFer.domain.report.entity.UserActivity; import com.capstone.BnagFer.global.common.ErrorCode; import lombok.RequiredArgsConstructor; @@ -13,6 +14,8 @@ @Service public class AccountsCommonService { + private final UserJpaRepository userJpaRepository; + public void checkUserProfile(User user) { if (user.getProfile() == null) throw new AccountsExceptionHandler(ErrorCode.PROFILE_NOT_EXIST); @@ -23,9 +26,9 @@ public void checkUserActivity(User user) { throw new AccountsExceptionHandler(ErrorCode.USER_IS_BANNED); } - public void validateStaffAccess(User user) { - if (!user.getIsStaff()) { - throw new AccountsExceptionHandler(ErrorCode.USER_IS_NOT_STAFF); + public void checkUserEmail(String email) { + if (userJpaRepository.existsByEmail(email)) { + throw new AccountsExceptionHandler(ErrorCode.EMAIL_ALREADY_EXIST); } } } \ No newline at end of file diff --git a/src/main/java/com/capstone/BnagFer/domain/accounts/service/account/AccountsService.java b/src/main/java/com/capstone/BnagFer/domain/accounts/service/account/AccountsService.java index 8344ab0f..32e44e86 100644 --- a/src/main/java/com/capstone/BnagFer/domain/accounts/service/account/AccountsService.java +++ b/src/main/java/com/capstone/BnagFer/domain/accounts/service/account/AccountsService.java @@ -31,6 +31,7 @@ public class AccountsService { private final PasswordEncoder passwordEncoder; private final JwtProvider jwtProvider; private final RedisUtil redisUtil; + private final AccountsCommonService accountsCommonService; public UserLoginResponseDto login(UserLoginRequestDto requestDto) { @@ -67,9 +68,7 @@ public UserSignupResponseDto signup(UserSignupRequestDto requestDto) { throw new AccountsExceptionHandler(ErrorCode.PASSWORD_NOT_EQUAL); // 이메일 중복 확인 - if (userJpaRepository.existsByEmail(requestDto.email())) { - throw new AccountsExceptionHandler(ErrorCode.USER_ALREADY_EXIST); - } + accountsCommonService.checkUserEmail(requestDto.email()); String encodedPw = passwordEncoder.encode(requestDto.password()); User user = requestDto.toEntity(encodedPw); @@ -144,9 +143,7 @@ public void updateEmail(HttpServletRequest request, User user, ChangeEmailReques } // 새 이메일이 이미 사용 중인지 확인 - if (userJpaRepository.existsByEmail(requestDto.newEmail())) { - throw new AccountsExceptionHandler(ErrorCode.EMAIL_ALREADY_EXIST); - } + accountsCommonService.checkUserEmail(requestDto.newEmail()); // 이메일 변경 user.updateEmail(requestDto); diff --git a/src/main/java/com/capstone/BnagFer/domain/tactic/controller/TacticController.java b/src/main/java/com/capstone/BnagFer/domain/tactic/controller/TacticController.java index 90b09641..c9ea09fb 100644 --- a/src/main/java/com/capstone/BnagFer/domain/tactic/controller/TacticController.java +++ b/src/main/java/com/capstone/BnagFer/domain/tactic/controller/TacticController.java @@ -92,8 +92,8 @@ public ApiResponse updateTactic(@PathVariable(name = "tacticId") @Operation(summary = "전술 복사후 가져오기", description = "다른 사람의 전술을 복사해서 작성자를 자신으로 하여 저장.") @PostMapping("/{tacticId}") - public ApiResponse copyTactic(@PathVariable(name = "tacticId") Long tacticId, @LoginUser User user) { - TacticResponse tacticDetail = tacticService.copyTactic(tacticId, user); + public ApiResponse copyTactic(@PathVariable(name = "tacticId") Long tacticId, @LoginUser User user) { + CopyTacticResponse tacticDetail = tacticService.copyTactic(tacticId, user); return ApiResponse.onSuccess(tacticDetail); } diff --git a/src/main/java/com/capstone/BnagFer/domain/tactic/dto/CopyTacticResponse.java b/src/main/java/com/capstone/BnagFer/domain/tactic/dto/CopyTacticResponse.java new file mode 100644 index 00000000..76d8a26a --- /dev/null +++ b/src/main/java/com/capstone/BnagFer/domain/tactic/dto/CopyTacticResponse.java @@ -0,0 +1,59 @@ +package com.capstone.BnagFer.domain.tactic.dto; + +import com.capstone.BnagFer.domain.tactic.entity.Position; +import com.capstone.BnagFer.domain.tactic.entity.Tactic; +import com.capstone.BnagFer.domain.tactic.entity.TacticPositionDetail; +import lombok.Builder; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Builder +public record CopyTacticResponse( + Long tacticId, + Long userId, + String nickname, + String tacticName, + Boolean anonymous, + String mainFormation, + List positionDetail, + String tacticDetails, + String subTactic, + LocalDateTime createdAt, + LocalDateTime updatedAt) { + public static CopyTacticResponse from(Tactic tactic) { + + return CopyTacticResponse.builder() + .tacticId(tactic.getTacticId()) + .userId(tactic.getUser().getId()) + .nickname(tactic.getUser().getProfile().getNickname()) + .tacticName(tactic.getTacticName()) + .anonymous(tactic.isAnonymous()) + .mainFormation(tactic.getMainFormation()) + .positionDetail(CopyTacticResponse.DetailList.from(tactic.getTacticPositionDetails())) + .tacticDetails(tactic.getTacticDetails()) + .subTactic(tactic.getSubTactic()) + .createdAt(tactic.getCreatedAt()) + .updatedAt(tactic.getUpdatedAt()) + .build(); + } + + @Builder + public record DetailList( + Long detailId, + Position position, + String positionDescription + ){ + public static TacticDetailResponse.DetailList from(TacticPositionDetail tacticPositionDetail){ + return TacticDetailResponse.DetailList.builder() + .detailId(tacticPositionDetail.getDetailId()) + .position(tacticPositionDetail.getPosition()) + .positionDescription(tacticPositionDetail.getPositionDescription()) + .build(); + } + public static List from(List positions){ + return positions.stream().map(TacticDetailResponse.DetailList::from).toList(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/capstone/BnagFer/domain/tactic/entity/Tactic.java b/src/main/java/com/capstone/BnagFer/domain/tactic/entity/Tactic.java index acdd514e..6bb2fe6d 100644 --- a/src/main/java/com/capstone/BnagFer/domain/tactic/entity/Tactic.java +++ b/src/main/java/com/capstone/BnagFer/domain/tactic/entity/Tactic.java @@ -85,4 +85,8 @@ public void updateTactic(TacticUpdateRequest request){ public static Tactic createTactic() { return new Tactic(); } + + public void setTacticPositionDetails(List tacticPositionDetails) { + this.tacticPositionDetails = tacticPositionDetails; + } } diff --git a/src/main/java/com/capstone/BnagFer/domain/tactic/entity/TacticPositionDetail.java b/src/main/java/com/capstone/BnagFer/domain/tactic/entity/TacticPositionDetail.java index a40a9d3a..d00a7197 100644 --- a/src/main/java/com/capstone/BnagFer/domain/tactic/entity/TacticPositionDetail.java +++ b/src/main/java/com/capstone/BnagFer/domain/tactic/entity/TacticPositionDetail.java @@ -32,4 +32,10 @@ public void updateDetail(DetailUpdateRequest request){ position = request.position(); positionDescription = request.positionDescription(); } + + public void setDetail(Tactic tactic, TacticPositionDetail request){ + this.tactic = tactic; + position = request.getPosition(); + positionDescription = request.getPositionDescription(); + } } diff --git a/src/main/java/com/capstone/BnagFer/domain/tactic/service/TacticService.java b/src/main/java/com/capstone/BnagFer/domain/tactic/service/TacticService.java index 20e173c8..48138422 100644 --- a/src/main/java/com/capstone/BnagFer/domain/tactic/service/TacticService.java +++ b/src/main/java/com/capstone/BnagFer/domain/tactic/service/TacticService.java @@ -20,6 +20,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -90,7 +91,8 @@ private void createPositionDetails(List positionDetails, Ta } } - public TacticResponse copyTactic(Long tacticId, User user) { + @Transactional + public CopyTacticResponse copyTactic(Long tacticId, User user) { Tactic tactic = tacticRepository.findById(tacticId).orElseThrow(() -> new TacticExceptionHandler(ErrorCode.TACTIC_NOT_FOUND)); @@ -100,11 +102,20 @@ public TacticResponse copyTactic(Long tacticId, User user) { Tactic copyTactic = Tactic.createTactic(); copyTactic.setCopyDetail(user, tactic); + // 깊은 복사 수행 + copyTactic.setTacticPositionDetails(new ArrayList<>()); + for (TacticPositionDetail detail : tactic.getTacticPositionDetails()) { + TacticPositionDetail copyDetail = new TacticPositionDetail(); + // TacticPositionDetail의 모든 필드를 복사 + copyDetail.setDetail(copyTactic, detail); + copyTactic.getTacticPositionDetails().add(copyDetail); + } + // 프로필 존재 확인 accountsCommonService.checkUserProfile(user); tacticRepository.save(copyTactic); - return TacticResponse.from(copyTactic); + return CopyTacticResponse.from(copyTactic); } public CommentResponse createComment(Long tacticId, CommentCreateRequest request, User user, Long parentCommentId){ @@ -198,4 +209,4 @@ private void sendNotification(User user, Tactic tactic, Long parentCommentId, Ta } } -} +} \ No newline at end of file diff --git a/src/main/java/com/capstone/BnagFer/global/common/ErrorCode.java b/src/main/java/com/capstone/BnagFer/global/common/ErrorCode.java index adbdf5c0..a47994c2 100644 --- a/src/main/java/com/capstone/BnagFer/global/common/ErrorCode.java +++ b/src/main/java/com/capstone/BnagFer/global/common/ErrorCode.java @@ -31,6 +31,7 @@ public enum ErrorCode implements BaseErrorCode { EMAIL_NOT_MATCH(HttpStatus.BAD_REQUEST, "USER414", "이메일이 맞지 않습니다."), CANNOT_USE_SAME_PASSWORD(HttpStatus.BAD_REQUEST, "USER415", "기존 비밀번호와 동일합니다."), USER_IS_BANNED(HttpStatus.BAD_REQUEST, "USER416", "사용이 정지된 유저입니다."), + EMAIL_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "USER417", "해당 이메일이 이미 존재합니다."), // Firebase 관련 에러 FIREBASE_MESSAGING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "FIREBASE401", "Firebase 메시징 예외가 발생했습니다."), @@ -47,7 +48,6 @@ public enum ErrorCode implements BaseErrorCode { PROFILE_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "PROFILE402", "프로필이 이미 존재합니다."), PROFILE_NOT_FOUND(HttpStatus.BAD_REQUEST, "PROFILE403", "해당 프로필이 존재하지 않습니다."), PROFILE_AND_USER_NOT_MATCHED(HttpStatus.BAD_REQUEST, "PROFILE404", "자신의 프로필이 아닙니다. 권한이 없습니다."), - EMAIL_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "PROFILE404", "해당 이메일이 이미 존재합니다."), // Tactic 관련 에러 TACTIC_NOT_FOUND(HttpStatus.BAD_REQUEST, "TACTIC401", "전술이 없습니다."), diff --git a/src/main/java/com/capstone/BnagFer/global/config/SecurityConfig.java b/src/main/java/com/capstone/BnagFer/global/config/SecurityConfig.java index d65c908c..1adf7b76 100644 --- a/src/main/java/com/capstone/BnagFer/global/config/SecurityConfig.java +++ b/src/main/java/com/capstone/BnagFer/global/config/SecurityConfig.java @@ -32,7 +32,7 @@ public class SecurityConfig { private final String[] swaggerUrls = {"/swagger-ui/**", "/v3/**"}; private final String[] authUrls = {"/", "/accounts/signup/**", "/accounts/social/**", "/accounts/login/**", - "/api/v1/auth", "/oauth/kakao/**", "/accounts/reissue/**", "/accounts/forgotPw/**", "/accounts/email/send-email", "/accounts/email/verify"}; + "/api/v1/auth", "/oauth/kakao/**", "/accounts/reissue/**", "/accounts/forgotPw/**", "/accounts/email/send-email", "/accounts/email/verify", "/accounts/checkEmail", "/accounts/recover/**"}; private final String[] allowedUrls = Stream.concat(Arrays.stream(swaggerUrls), Arrays.stream(authUrls)) .toArray(String[]::new); diff --git a/src/main/java/com/capstone/BnagFer/global/util/RedisUtil.java b/src/main/java/com/capstone/BnagFer/global/util/RedisUtil.java index 6cf71761..ef1ace2d 100644 --- a/src/main/java/com/capstone/BnagFer/global/util/RedisUtil.java +++ b/src/main/java/com/capstone/BnagFer/global/util/RedisUtil.java @@ -1,19 +1,26 @@ package com.capstone.BnagFer.global.util; + import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; + @Component @RequiredArgsConstructor public class RedisUtil { private final RedisTemplate redisTemplate; + private static final long ONE_WEEK_IN_SECONDS = 7 * 24 * 60 * 60; // 일주일을 초로 표현 public void save(String key, Object val, Long time, TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, val, time, timeUnit); } + private void saveWithOneWeekTTL(String key, Object val) { + redisTemplate.opsForValue().set(key, val, ONE_WEEK_IN_SECONDS, TimeUnit.SECONDS); + } + public void saveLikeCount(Long tacticId, Long likeCount) { - redisTemplate.opsForValue().set("tactic:" + tacticId + ":likeCount", likeCount.toString()); + saveWithOneWeekTTL("tactic:" + tacticId + ":likeCount", likeCount.toString()); } public Long getLikeCount(Long tacticId) { @@ -22,7 +29,7 @@ public Long getLikeCount(Long tacticId) { } public void boardSaveLikeCount(Long boardId, Long likeCount) { - redisTemplate.opsForValue().set("board:" + boardId + ":likeCount", likeCount.toString()); + saveWithOneWeekTTL("board:" + boardId + ":likeCount", likeCount.toString()); } public Long boardGetLikeCount(Long boardId) { @@ -31,19 +38,19 @@ public Long boardGetLikeCount(Long boardId) { } public void boardSaveCommentCount(Long boardId, Long commentCount) { - redisTemplate.opsForValue().set("board:" + boardId + ":commentCount", commentCount.toString()); + saveWithOneWeekTTL("board:" + boardId + ":commentCount", commentCount.toString()); } - public Long boardGetCommentCount(Long boardId){ + public Long boardGetCommentCount(Long boardId) { String commentCountStr = (String) redisTemplate.opsForValue().get("board:" + boardId + ":commentCount"); return commentCountStr != null ? Long.valueOf(commentCountStr) : null; } - public void saveCommentCount(Long tacticId, Long commentCount){ - redisTemplate.opsForValue().set("tactic:" + tacticId + ":commentCount", commentCount.toString()); + public void saveCommentCount(Long tacticId, Long commentCount) { + saveWithOneWeekTTL("tactic:" + tacticId + ":commentCount", commentCount.toString()); } - public Long getCommentCount(Long tacticId){ + public Long getCommentCount(Long tacticId) { String commentCountStr = (String) redisTemplate.opsForValue().get("tactic:" + tacticId + ":commentCount"); return commentCountStr != null ? Long.valueOf(commentCountStr) : null; } @@ -64,6 +71,7 @@ public void saveFCMToken(String userEmail, String fcmToken) { redisTemplate.opsForValue().set(userEmail, fcmToken); redisTemplate.expire(userEmail, 30, TimeUnit.DAYS); } + public String getFCMToken(String userEmail) { Object tokenObj = redisTemplate.opsForValue().get(userEmail); if (tokenObj != null) { @@ -72,6 +80,7 @@ public String getFCMToken(String userEmail) { return null; } } + public void removeFCMToken(String userEmail) { redisTemplate.delete(userEmail); }