Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 임시 로그인 기능 구현 #16

Merged
merged 11 commits into from
Jan 18, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.wedit.weditapp.domain.member.controller;

import com.wedit.weditapp.domain.member.domain.Member;
import com.wedit.weditapp.domain.member.dto.LoginRequestDto;
import com.wedit.weditapp.domain.member.dto.MemberRequestDto;
import com.wedit.weditapp.domain.member.dto.MemberResponseDto;
import com.wedit.weditapp.domain.member.service.MemberService;
import com.wedit.weditapp.global.response.GlobalResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/members")
@RequiredArgsConstructor
public class MemberController {

private final MemberService memberService;

// 회원 가입 API
@Operation(summary = "회원가입", description = "새로운 유저를 등록합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "회원 가입 성공"),
@ApiResponse(responseCode = "409", description = "이미 가입된 이메일입니다."),
@ApiResponse(responseCode = "500", description = "서버 에러")
})
@PostMapping("/signup")
public ResponseEntity<GlobalResponseDto<MemberResponseDto>> createMember(@Valid @RequestBody MemberRequestDto requestDto) {
Member saved = memberService.createMember(requestDto);

return ResponseEntity
.status(HttpStatus.CREATED)
.body(GlobalResponseDto.success(MemberResponseDto.from(saved), 201));
}

// 임시 로그인 API
@Operation(summary = "임시 로그인", description = "해당 서비스에 로그인합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "로그인 성공"),
@ApiResponse(responseCode = "401", description = "로그인에 실패하였습니다."),
@ApiResponse(responseCode = "404", description = "유저를 찾을 수 없습니다."),
@ApiResponse(responseCode = "500", description = "서버 에러")
})
@PostMapping("/login")
public ResponseEntity<GlobalResponseDto<String>> login(@RequestBody LoginRequestDto loginRequest) {
// 로그인 로직 → 토큰 발급
String token = memberService.login(loginRequest);

// 토큰을 응답 바디로 전달
return ResponseEntity.ok(GlobalResponseDto.success(token));
}

// 모든 회원 조회 API
@Operation(summary = "모든 회원 조회", description = "등록된 모든 회원의 정보를 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "조회 성공"),
@ApiResponse(responseCode = "404", description = "유저를 찾을 수 없습니다."),
@ApiResponse(responseCode = "500", description = "서버 에러")
})
@GetMapping
public ResponseEntity<GlobalResponseDto<List<MemberResponseDto>>> findAllMembers() {
List<Member> members = memberService.findAllMembers();
List<MemberResponseDto> memberResponses = members.stream()
.map(MemberResponseDto::from)
.collect(Collectors.toList());

return ResponseEntity.ok(GlobalResponseDto.success(memberResponses));
}

// 단일 회원 조회 API
@Operation(summary = "단일 회원 조회", description = "해당 id를 가진 회원의 정보를 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "조회 성공"),
@ApiResponse(responseCode = "404", description = "유저를 찾을 수 없습니다."),
@ApiResponse(responseCode = "500", description = "서버 에러")
})
@GetMapping("/{userId}")
public ResponseEntity<GlobalResponseDto<MemberResponseDto>> findMemberById(@PathVariable Long userId) {
Member member = memberService.findMemberById(userId);
return ResponseEntity.ok(GlobalResponseDto.success(MemberResponseDto.from(member)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,30 +32,25 @@ public class Member extends BaseTimeEntity {
@Column(name = "role", nullable = false)
private MemberRole role;

@Column(nullable = false, unique = true)
private String refresh_token;

@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private MemberStatus status;

// Builder를 통해서만 객체를 생성하도록 (일반 생성자는 protected)
@Builder
private Member(String kakaoId, String email, String name, MemberRole role, String password, MemberStatus status) {
private Member(String email, String password, String name, MemberRole role, MemberStatus status) {
this.email = email;
this.password = password;
this.name = name;
this.role = role != null ? role : MemberRole.USER; // 유저 역할 기본값 USER
this.password = password;
this.status = status != null ? status : MemberStatus.ACTIVE; // 유저 상태 기본값 ACTIVE
}

public static Member createUser(String email, String name, String password) {
public static Member createUser(String email, String password, String name) {
return Member.builder()
.email(email)
.name(name)
.password(password)
.role(MemberRole.USER) // 기본값 USER
.status(MemberStatus.ACTIVE) // 기본값 ACTIVE
.name(name)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.wedit.weditapp.domain.member.domain.repository;

import com.wedit.weditapp.domain.member.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {
// 임시 로그인용 : 이메일로 회원 찾기
Optional<Member> findByEmail(String email);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.wedit.weditapp.domain.member.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class LoginRequestDto { // 임시 로그인Dto - 나중에 수정해야 함
private String email;
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.wedit.weditapp.domain.member.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class MemberRequestDto {
@Email(message = "이메일 형식을 지켜야 합니다.")
@NotBlank(message = "이메일을 반드시 입력해야 합니다.")
private String email;

@NotBlank(message = "비밀번호를 반드시 입력해야 합니다.")
private String password; // 임시 비밀번호

@NotBlank(message = "이름을 반드시 입력해야 합니다.")
private String name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.wedit.weditapp.domain.member.dto;

import com.wedit.weditapp.domain.member.domain.Member;
import com.wedit.weditapp.domain.shared.MemberRole;
import com.wedit.weditapp.domain.shared.MemberStatus;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class MemberResponseDto {
private Long id;
private String email;
private String name;
private MemberRole role;
private MemberStatus status;

public static MemberResponseDto from(Member member) {
return MemberResponseDto.builder()
.id(member.getId())
.email(member.getEmail())
.name(member.getName())
.role(member.getRole())
.status(member.getStatus())
.build();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.wedit.weditapp.domain.member.service;

import com.wedit.weditapp.domain.member.domain.Member;
import com.wedit.weditapp.domain.member.domain.repository.MemberRepository;
import com.wedit.weditapp.domain.member.dto.LoginRequestDto;
import com.wedit.weditapp.domain.member.dto.MemberRequestDto;
import com.wedit.weditapp.global.error.ErrorCode;
import com.wedit.weditapp.global.error.exception.CommonException;
import com.wedit.weditapp.global.security.jwt.JwtProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final JwtProvider jwtProvider; // JWT 발급을 위해

// [회원가입 관련] - 이미 존재하는 이메일인지 검사 + Member 엔티티 생성 + DB저장 및 반환
public Member createMember(MemberRequestDto requestDto) {
if (memberRepository.findByEmail(requestDto.getEmail()).isPresent()) {
throw new CommonException(ErrorCode.MEMBER_ALREADY_EXISTS);
}

Member member = Member.createUser(
requestDto.getEmail(),
requestDto.getPassword(),
requestDto.getName()
);

return memberRepository.save(member);
}

public String login(LoginRequestDto loginRequest) {
// 이메일로 회원 조회
Member member = memberRepository.findByEmail(loginRequest.getEmail())
.orElseThrow(() -> new CommonException(ErrorCode.USER_NOT_FOUND));

// 비밀번호 검증 (임시: 평문 비교)
if (!member.getPassword().equals(loginRequest.getPassword())) {
throw new CommonException(ErrorCode.LOGIN_FAIL);
}

// JWT Access Token 생성
return jwtProvider.createAccessToken(member.getEmail());
}

// [모든 회원 조회]
public List<Member> findAllMembers() {
return memberRepository.findAll();
}

// [단일 회원 조회]
public Member findMemberById(Long userId) {
return memberRepository.findById(userId)
.orElseThrow(() -> new CommonException(ErrorCode.USER_NOT_FOUND)
);
}

}
31 changes: 26 additions & 5 deletions src/main/java/com/wedit/weditapp/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@

import java.util.Arrays;

import com.wedit.weditapp.global.security.jwt.JwtAuthenticationFilter;
import com.wedit.weditapp.global.security.jwt.JwtProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
Expand All @@ -22,13 +28,19 @@
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.cors(withDefaults())
.csrf(AbstractHttpConfigurer::disable)

// 세션 관련 정책 추가 : 세션 방식 사용 X (오직 JWT만 사용)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

.headers(
headersConfigurer ->
headersConfigurer
Expand All @@ -39,26 +51,35 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.authorizeHttpRequests(
authorize ->
authorize
.requestMatchers( "/v3/api-docs/**", "/swagger-ui/**").permitAll() // swagger-ui 접근 허용
.anyRequest().authenticated()
.requestMatchers( "/v3/api-docs/**", "/swagger-ui/**", "/api/members/login", "/api/members/signup").permitAll() // swagger-ui 접근 허용 + 로그인과 회원가입까지만 허용
.anyRequest().authenticated() // 그외에는 Access Token을 가진 유저만 접근 가능
);

// JWT 인증 필터 등록
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();

configuration.setAllowedOrigins(Arrays.asList());
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setExposedHeaders(Arrays.asList("Authorization, Authorization_refresh", "accept"));
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:8080"));
configuration.setExposedHeaders(Arrays.asList("Authorization", "Authorization_refresh", "accept"));
configuration.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}

// 회원가입 시 사용
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

}
Loading
Loading