Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'

//Database
runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'com.mysql:mysql-connector-j:8.0.33'

//Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.adoonge.seedzip.auth.controller;

import com.adoonge.seedzip.auth.dto.request.BasicLoginRequest;
import com.adoonge.seedzip.auth.dto.request.BasicSignUpRequest;
import com.adoonge.seedzip.auth.dto.request.SignUpRequest;
import com.adoonge.seedzip.auth.dto.response.BasicLoginResponse;
import com.adoonge.seedzip.auth.dto.response.LoginResponse;
import com.adoonge.seedzip.auth.service.AuthService;
import com.adoonge.seedzip.auth.util.CustomUserDetails;
Expand All @@ -12,6 +15,9 @@
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -26,6 +32,7 @@
@RequiredArgsConstructor
@Tag(name = "AuthController", description = "회원 인증 관련 API")
public class AuthController {

private final AuthService authService;

/**
Expand All @@ -50,6 +57,16 @@ ApiResponse<LoginResponse> loginKakaoForApp(@RequestParam String accessToken, @P
return authService.loginForApp(accessToken, socialType.toUpperCase(), response);
}

@Operation(
summary = "기본 로그인",
description = "이메일과 비밀번호로 로그인합니다. '@'를 포함한 이메일과 비밀번호를 입력해주세요.")
@PostMapping("/login/basic")
public ApiResponse<BasicLoginResponse>basicLogin(
@Valid @RequestBody BasicLoginRequest basicLoginRequest, HttpServletResponse response) {

return authService.basicLogin(basicLoginRequest, response);
}

@GetMapping("/test")
@Operation(summary = "로그인 테스트 API", description = "로그인 여부를 확인할 수 있는 API입니다. 회원의 닉네임을 리턴합니다.")
public ApiResponse<String> test(@AuthenticationPrincipal CustomUserDetails customUserDetails) {
Expand All @@ -64,12 +81,27 @@ public ApiResponse<String> test(@AuthenticationPrincipal CustomUserDetails custo
*/
@PostMapping("/signup")
@Operation(summary = "회원가입 API", description = "회원가입을 진행하는 API입니다. (SocialType : BASIC, GOOGLE, NAVER, KAKAO")
public ApiResponse<Void> signUp(@RequestBody @Valid SignUpRequest signUpRequest, HttpServletResponse response) {
public ApiResponse<Void> signUp(
@RequestBody @Valid SignUpRequest signUpRequest, HttpServletResponse response) {

authService.signUp(signUpRequest, response);

return new ApiResponse<>(ErrorCode.REQUEST_OK);
}

@PostMapping("/signup/basic")
@Operation(
summary = "기본 회원가입",
description = "이메일과 비밀번호로 회원가입합니다.\n"
+ "- 이메일 : @를 포함한 이메일 형식이어야 합니다.\n"
+ "- 비밀번호 : 비밀번호는 영문자와 숫자를 포함한 8~20자여야 합니다")
public ApiResponse<Void> basicSignUp(
@RequestBody @Valid BasicSignUpRequest basicSignUpRequest, HttpServletResponse response) {

authService.basicSignUp(basicSignUpRequest);

return new ApiResponse<>(ErrorCode.REQUEST_OK);
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.adoonge.seedzip.auth.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;

@Builder
public record BasicLoginRequest(@Email String email, @NotNull String password) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.adoonge.seedzip.auth.dto.request;

import java.time.LocalDate;

import com.adoonge.seedzip.member.domain.Gender;
import com.adoonge.seedzip.member.domain.Member;
import com.adoonge.seedzip.member.domain.Role;

import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PastOrPresent;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class BasicSignUpRequest{

@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "유효한 이메일 형식이어야 합니다.")
private String email;

@NotBlank(message = "비밀번호는 필수입니다.")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,20}$", message = "비밀번호는 영문자와 숫자를 포함한 8~20자여야 합니다.")
private String password;

@NotBlank(message = "닉네임은 필수입니다.")
@Pattern(regexp = "^[가-힣a-zA-Z0-9 ]{1,10}$", message = "닉네임은 한글, 영문, 숫자, 공백 포함 10자 이내로 작성해야 합니다.")
private String nickname;

@NotNull(message = "생년월일은 필수입니다.")
@PastOrPresent(message = "생년월일은 과거 또는 오늘 날짜여야 합니다.")
private LocalDate birthday;

private Gender gender;
private String occupation;
private String field;

// 필수 - 서비스 이용 약관 동의
@AssertTrue(message = "서비스 이용 약관에 동의해야 합니다.")
private Boolean consentToTermsOfService;

// 필수 - 개인정보 수집 및 이용 동의
@AssertTrue(message = "개인정보 수집 및 이용에 동의해야 합니다.")
private Boolean consentToPersonalInformation;

private Boolean consentToMarketingAndAds; // 마케팅 활용 및 광고성 정보 수신 동의 여부

public Member toEntity(String email, String encodedPassword, String profileImageUrl) {
return Member.builder()
.loginId(email)
.password(encodedPassword)
.nickname(nickname)
.birthday(birthday)
.gender(gender)
.occupation(occupation)
.field(field)
.role(Role.USER)
.profileImageUrl(profileImageUrl)
.consentToTermsOfService(consentToTermsOfService)
.consentToPersonalInformation(consentToPersonalInformation)
.consentToMarketingAndAds(consentToMarketingAndAds)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.adoonge.seedzip.auth.dto.response;

import com.adoonge.seedzip.auth.domain.SocialType;

import lombok.Builder;

@Builder
public record BasicLoginResponse(String result, SocialType socialType) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
// Request Header에서 토큰 정보 추출
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) {
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/com/adoonge/seedzip/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.adoonge.seedzip.auth.service;

import com.adoonge.seedzip.auth.domain.SocialType;
import com.adoonge.seedzip.auth.dto.request.BasicLoginRequest;
import com.adoonge.seedzip.auth.dto.request.BasicSignUpRequest;
import com.adoonge.seedzip.auth.dto.request.SignUpRequest;
import com.adoonge.seedzip.auth.dto.response.BasicLoginResponse;
import com.adoonge.seedzip.auth.dto.response.LoginResponse;
import com.adoonge.seedzip.category.domain.Category;
import com.adoonge.seedzip.category.repository.CategoryRepository;
Expand All @@ -14,13 +17,16 @@
import com.adoonge.seedzip.auth.service.oauth.OAuthServiceFactory;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService {
Expand Down Expand Up @@ -81,6 +87,36 @@ public ApiResponse<LoginResponse> loginForApp(String accessToken, String input,
return new ApiResponse<>(LoginResponse.builder().result("").socialType(socialType).build(), ErrorCode.REQUEST_OK);
}

@Transactional(readOnly = true)
public ApiResponse<BasicLoginResponse> basicLogin(BasicLoginRequest basicLoginRequest, HttpServletResponse response) {
Member member =
memberRepository
.findByLoginId(basicLoginRequest.email())
.orElseThrow(() -> SeedzipException.from(ErrorCode.MEMBER_NOT_FOUND));

if (!passwordEncoder.matches(basicLoginRequest.password(), member.getPassword())) {
throw SeedzipException.from(ErrorCode.INVALID_CREDENTIALS);
}

UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(
basicLoginRequest.email(),
basicLoginRequest.password()
);

Authentication authentication =
authenticationManagerBuilder.getObject().authenticate(authenticationToken);

jwtTokenService.generateToken(authentication, response);

return new ApiResponse<>(
BasicLoginResponse.builder()
.result("")
.socialType(SocialType.BASIC)
.build(),
ErrorCode.REQUEST_OK);
}

@Transactional
public void signUp(SignUpRequest request, HttpServletResponse response) {

Expand Down Expand Up @@ -114,6 +150,24 @@ public void signUp(SignUpRequest request, HttpServletResponse response) {
generateToken(loginId, response);
}

@Transactional
public void basicSignUp(BasicSignUpRequest request) {
String email = request.getEmail();
if (memberRepository.existsByLoginId(email)) {
throw SeedzipException.from(ErrorCode.ACCOUNT_USERNAME_EXIST);
}

String encodedPassword = passwordEncoder.encode(request.getPassword());

Member member = request.toEntity(email ,encodedPassword, null); // 기본 프로필 이미지 추가해야함

memberRepository.save(member);
memberRepository.flush();

Category category = Category.builder().name("미분류").member(member).isPublic(true).isDefault(true).build();
categoryRepository.save(category);
}

private void generateToken(String loginId, HttpServletResponse response) {
// 1. username + password 를 기반으로 Authentication 객체 생성
// 이때 authentication 은 인증 여부를 확인하는 authenticated 값이 false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,26 @@ public record CategoryResponse(
Long categoryId,
String name,
Boolean isPublic,
Long memberId) {
Long memberId,
Long seedCount) {

public static CategoryResponse fromEntityWithCount(Category category, Long seedCount) {
return new CategoryResponse(
category.getCategoryId(),
category.getName(),
category.getIsPublic(),
category.getMember().getId(),
seedCount
);
}

public static CategoryResponse fromEntity(Category category) {
return new CategoryResponse(
category.getCategoryId(),
category.getName(),
category.getIsPublic(),
category.getMember().getId()
category.getMember().getId(),
0L
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.adoonge.seedzip.global.exception.ErrorCode;
import com.adoonge.seedzip.global.exception.SeedzipException;
import com.adoonge.seedzip.member.domain.Member;
import com.adoonge.seedzip.seed.repository.CategorySeedRepository;
import com.adoonge.seedzip.seed.repository.SeedRepository;

import lombok.AccessLevel;
Expand All @@ -27,6 +28,7 @@ public class CategoryService {

private final CategoryRepository categoryRepository;
private final SeedRepository seedRepository;
private final CategorySeedRepository categorySeedRepository;

private static final String DEFAULT_CATEGORY_NAME = "미분류";

Expand Down Expand Up @@ -72,7 +74,10 @@ public List<CategoryResponse> getCategories(Member member) {
List<Category> categories = categoryRepository.findAllByMemberOrdered(member);

return categories.stream()
.map(CategoryResponse::fromEntity)
.map( category -> {
Long seedCount = categorySeedRepository.countByCategoryCategoryId(category.getCategoryId());
return CategoryResponse.fromEntityWithCount(category, seedCount);
})
.toList();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Excepti
.requestMatchers("/swagger", "/swagger-ui.html", "/swagger-ui/**", "/api-docs", "/api-docs/**", "/v1/api-docs/**","/health/**")
.permitAll()
// 해당 API에 대해서는 모든 요청을 허가
.requestMatchers("/api/v1/auth/login/**","/api/v1/auth/signup","/api/v1/simplification/**").permitAll()
.requestMatchers("/api/v1/auth/login/**","/api/v1/auth/signup/**","/api/v1/simplification/**")
.permitAll()
//.requestMatchers("/members/test").hasRole("USER")
// USER 권한이 있어야 요청할 수 있음
//.requestMatchers("/members/test").hasRole("USER")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,23 @@ public ApiResponse(List<T> results) {
// Page 타입 생성자
public ApiResponse(Page<T> page) {
this.status = new Status(ErrorCode.REQUEST_OK);
this.metadata = new Metadata(page.getContent().size(), page.getPageable(), page.hasNext());
this.metadata = new Metadata(
page.getTotalElements(),
page.getContent().size(),
page.getPageable(),
page.hasNext());
this.results = page.getContent();
}

// Slice 타입 생성자
public ApiResponse(Slice<T> slice) {
this.status = new Status(ErrorCode.REQUEST_OK);
this.metadata = new Metadata(slice.getContent().size(), slice.getPageable(), slice.hasNext());
this.results = slice.getContent();
}
// public ApiResponse(Slice<T> slice) {
// this.status = new Status(ErrorCode.REQUEST_OK);
// this.metadata = new Metadata(
// slice.getContent().size(),
// slice.getPageable(),
// slice.hasNext());
// this.results = slice.getContent();
// }

// 오류 코드 생성자
public ApiResponse(ErrorCode errorCode) {
Expand All @@ -66,6 +73,7 @@ public ApiResponse(SeedzipException exception) {

@Getter
private static class Metadata {
private long totalElementCount = 0;
private int resultCount = 0;
@JsonInclude(Include.NON_NULL) // 값이 설정되지 않으면 JSON 응답에서 제외
private Pageable pageable;
Expand All @@ -78,7 +86,8 @@ public Metadata(int resultCount) {
}

// Page용 메타데이터 생성자
public Metadata(int resultCount, Pageable pageable, boolean hasNext) {
public Metadata(long totalElementCount, int resultCount, Pageable pageable, boolean hasNext) {
this.totalElementCount = totalElementCount;
this.resultCount = resultCount;
this.pageable = pageable;
this.hasNext = hasNext;
Expand Down
Loading
Loading