From 77f8ab44d015ebf76860dd06cb8f77927bee966a Mon Sep 17 00:00:00 2001 From: JunHyeongChoi Date: Mon, 4 Nov 2024 21:40:09 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9D=B5=EB=AA=85=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../everymoment/config/SecurityConfig.java | 3 +- .../everymoment/config/SwaggerConfig.java | 2 +- .../controller/MemberController.java | 10 ++++++ .../dto/response/AnonymousLoginResponse.java | 15 ++++++++ .../repository/MemberRepository.java | 7 ++++ .../everymoment/service/MemberService.java | 36 +++++++++++++++++++ 6 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/potatocake/everymoment/dto/response/AnonymousLoginResponse.java diff --git a/src/main/java/com/potatocake/everymoment/config/SecurityConfig.java b/src/main/java/com/potatocake/everymoment/config/SecurityConfig.java index 97955c9..a921809 100644 --- a/src/main/java/com/potatocake/everymoment/config/SecurityConfig.java +++ b/src/main/java/com/potatocake/everymoment/config/SecurityConfig.java @@ -45,7 +45,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/members/login", "/h2-console/**", "/error", "/favicon.ico").permitAll() + .requestMatchers("/api/members/login", "/api/members/anonymous-login", "/h2-console/**", + "/error", "/favicon.ico").permitAll() .requestMatchers("/swagger-resources/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll() .anyRequest().authenticated()); diff --git a/src/main/java/com/potatocake/everymoment/config/SwaggerConfig.java b/src/main/java/com/potatocake/everymoment/config/SwaggerConfig.java index bfac4c8..9acbedd 100644 --- a/src/main/java/com/potatocake/everymoment/config/SwaggerConfig.java +++ b/src/main/java/com/potatocake/everymoment/config/SwaggerConfig.java @@ -39,7 +39,7 @@ private OpenApiCustomizer addSecurityItemToAllEndpointsExceptLogin() { return openApi -> { SecurityRequirement securityRequirement = new SecurityRequirement().addList("bearerAuth"); openApi.getPaths().forEach((path, item) -> { - if (!"/api/members/login".equals(path)) { + if (!"/api/members/login".equals(path) && !"/api/members/anonymous-login".equals(path)) { item.readOperations().forEach(operation -> { operation.addSecurityItem(securityRequirement); }); diff --git a/src/main/java/com/potatocake/everymoment/controller/MemberController.java b/src/main/java/com/potatocake/everymoment/controller/MemberController.java index 894c9d6..67cf2ad 100644 --- a/src/main/java/com/potatocake/everymoment/controller/MemberController.java +++ b/src/main/java/com/potatocake/everymoment/controller/MemberController.java @@ -2,6 +2,7 @@ import com.potatocake.everymoment.dto.SuccessResponse; import com.potatocake.everymoment.dto.request.MemberLoginRequest; +import com.potatocake.everymoment.dto.response.AnonymousLoginResponse; import com.potatocake.everymoment.dto.response.JwtResponse; import com.potatocake.everymoment.dto.response.MemberDetailResponse; import com.potatocake.everymoment.dto.response.MemberMyResponse; @@ -39,6 +40,15 @@ public class MemberController { private final MemberService memberService; + @Operation(summary = "익명 로그인", description = "회원번호로 로그인하거나 새로운 익명 계정을 생성합니다.") + @ApiResponse(responseCode = "200", description = "익명 로그인 성공") + @GetMapping("/anonymous-login") + public ResponseEntity anonymousLogin(@Parameter(description = "기기에 저장된 회원번호 (없을 경우 새로운 계정 생성)") + @RequestParam(required = false) Long number) { + AnonymousLoginResponse response = memberService.anonymousLogin(number); + return ResponseEntity.ok(SuccessResponse.ok(response)); + } + @Operation(summary = "로그인", description = "회원 번호와 닉네임으로 로그인합니다.") @ApiResponse(responseCode = "200", description = "로그인 성공", content = @Content(schema = @Schema(implementation = JwtResponse.class))) @PostMapping("/login") diff --git a/src/main/java/com/potatocake/everymoment/dto/response/AnonymousLoginResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/AnonymousLoginResponse.java new file mode 100644 index 0000000..c60203b --- /dev/null +++ b/src/main/java/com/potatocake/everymoment/dto/response/AnonymousLoginResponse.java @@ -0,0 +1,15 @@ +package com.potatocake.everymoment.dto.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Getter; + +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +public class AnonymousLoginResponse { + + private Long number; + private String token; + +} diff --git a/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java b/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java index 135e588..7f318ed 100644 --- a/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java +++ b/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java @@ -1,11 +1,14 @@ package com.potatocake.everymoment.repository; import com.potatocake.everymoment.entity.Member; +import jakarta.persistence.LockModeType; import java.util.Optional; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Window; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; public interface MemberRepository extends JpaRepository { @@ -15,4 +18,8 @@ public interface MemberRepository extends JpaRepository { Window findByNicknameContaining(String nickname, ScrollPosition position, Pageable pageable); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT CASE WHEN MIN(m.number) > 0 OR MIN(m.number) IS NULL THEN -1 ELSE MIN(m.number) - 1 END FROM Member m") + Long findNextAnonymousNumber(); + } diff --git a/src/main/java/com/potatocake/everymoment/service/MemberService.java b/src/main/java/com/potatocake/everymoment/service/MemberService.java index faad94e..bf60761 100644 --- a/src/main/java/com/potatocake/everymoment/service/MemberService.java +++ b/src/main/java/com/potatocake/everymoment/service/MemberService.java @@ -2,6 +2,7 @@ import static org.springframework.data.domain.Sort.Direction.ASC; +import com.potatocake.everymoment.dto.response.AnonymousLoginResponse; import com.potatocake.everymoment.dto.response.FriendRequestStatus; import com.potatocake.everymoment.dto.response.MemberDetailResponse; import com.potatocake.everymoment.dto.response.MemberMyResponse; @@ -13,6 +14,7 @@ import com.potatocake.everymoment.repository.FriendRepository; import com.potatocake.everymoment.repository.FriendRequestRepository; import com.potatocake.everymoment.repository.MemberRepository; +import com.potatocake.everymoment.util.JwtUtil; import com.potatocake.everymoment.util.PagingUtil; import com.potatocake.everymoment.util.S3FileUploader; import java.util.List; @@ -35,6 +37,7 @@ public class MemberService { private final FriendRepository friendRepository; private final PagingUtil pagingUtil; private final S3FileUploader s3FileUploader; + private final JwtUtil jwtUtil; @Transactional(readOnly = true) public MemberSearchResponse searchMembers(String nickname, Long key, int size, Long currentMemberId) { @@ -72,6 +75,39 @@ public MemberMyResponse getMemberInfo(Long memberId) { .build(); } + public AnonymousLoginResponse anonymousLogin(Long memberNumber) { + if (memberNumber != null) { + // 기존 회원번호로 로그인 시도 + return memberRepository.findByNumber(memberNumber) + .map(member -> AnonymousLoginResponse.builder() + .token(jwtUtil.create(member.getId())) + .build()) + .orElseGet(this::createAnonymousLoginResponse); + } + + // 새로운 익명 회원 생성 및 응답 + return createAnonymousLoginResponse(); + } + + private AnonymousLoginResponse createAnonymousLoginResponse() { + Member newMember = createAnonymousMember(); + return AnonymousLoginResponse.builder() + .number(newMember.getNumber()) + .token(jwtUtil.create(newMember.getId())) + .build(); + } + + private Member createAnonymousMember() { + Long nextNumber = memberRepository.findNextAnonymousNumber(); + + Member member = Member.builder() + .nickname("Anonymous") + .number(nextNumber) + .build(); + + return memberRepository.save(member); + } + public void updateMemberInfo(Long id, MultipartFile profileImage, String nickname) { Member member = memberRepository.findById(id) .orElseThrow(() -> new GlobalException(ErrorCode.MEMBER_NOT_FOUND));