add : init project

This commit is contained in:
yuhalog
2025-06-11 09:28:32 +09:00
commit b68c7c5fa1
105 changed files with 6022 additions and 0 deletions
@@ -0,0 +1,20 @@
package com.won.smarketing.member;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
/**
* 회원 서비스 메인 애플리케이션 클래스
* Spring Boot 애플리케이션의 진입점
*/
@SpringBootApplication(scanBasePackages = {"com.won.smarketing.member", "com.won.smarketing.common"})
@EntityScan(basePackages = {"com.won.smarketing.member.entity"})
@EnableJpaRepositories(basePackages = {"com.won.smarketing.member.repository"})
public class MemberServiceApplication {
public static void main(String[] args) {
SpringApplication.run(MemberServiceApplication.class, args);
}
}
@@ -0,0 +1,68 @@
package com.won.smarketing.member.controller;
import com.won.smarketing.common.dto.ApiResponse;
import com.won.smarketing.member.dto.LoginRequest;
import com.won.smarketing.member.dto.LoginResponse;
import com.won.smarketing.member.dto.LogoutRequest;
import com.won.smarketing.member.dto.TokenRefreshRequest;
import com.won.smarketing.member.dto.TokenResponse;
import com.won.smarketing.member.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/**
* 인증/인가를 위한 REST API 컨트롤러
* 로그인, 로그아웃, 토큰 갱신 기능 제공
*/
@Tag(name = "인증/인가", description = "로그인, 로그아웃, 토큰 관리 API")
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
/**
* 로그인 인증
*
* @param request 로그인 요청 정보
* @return JWT 토큰 정보
*/
@Operation(summary = "로그인", description = "사용자 인증 후 JWT 토큰을 발급합니다.")
@PostMapping("/login")
public ResponseEntity<ApiResponse<LoginResponse>> login(@Valid @RequestBody LoginRequest request) {
LoginResponse response = authService.login(request);
return ResponseEntity.ok(ApiResponse.success(response, "로그인이 완료되었습니다."));
}
/**
* 로그아웃 처리
*
* @param request 로그아웃 요청 정보
* @return 로그아웃 성공 응답
*/
@Operation(summary = "로그아웃", description = "사용자를 로그아웃하고 토큰을 무효화합니다.")
@PostMapping("/logout")
public ResponseEntity<ApiResponse<Void>> logout(@Valid @RequestBody LogoutRequest request) {
authService.logout(request.getRefreshToken());
return ResponseEntity.ok(ApiResponse.success(null, "로그아웃이 완료되었습니다."));
}
/**
* 토큰 갱신
*
* @param request 토큰 갱신 요청 정보
* @return 새로운 JWT 토큰 정보
*/
@Operation(summary = "토큰 갱신", description = "Refresh Token을 사용하여 새로운 Access Token을 발급합니다.")
@PostMapping("/refresh")
public ResponseEntity<ApiResponse<TokenResponse>> refresh(@Valid @RequestBody TokenRefreshRequest request) {
TokenResponse response = authService.refresh(request.getRefreshToken());
return ResponseEntity.ok(ApiResponse.success(response, "토큰이 갱신되었습니다."));
}
}
@@ -0,0 +1,74 @@
package com.won.smarketing.member.controller;
import com.won.smarketing.common.dto.ApiResponse;
import com.won.smarketing.member.dto.DuplicateCheckResponse;
import com.won.smarketing.member.dto.PasswordValidationRequest;
import com.won.smarketing.member.dto.RegisterRequest;
import com.won.smarketing.member.dto.ValidationResponse;
import com.won.smarketing.member.service.MemberService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/**
* 회원 관리를 위한 REST API 컨트롤러
* 회원가입, ID 중복 확인, 패스워드 유효성 검증 기능 제공
*/
@Tag(name = "회원 관리", description = "회원가입 및 회원 정보 관리 API")
@RestController
@RequestMapping("/api/member")
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
/**
* 회원가입 처리
*
* @param request 회원가입 요청 정보
* @return 회원가입 성공/실패 응답
*/
@Operation(summary = "회원가입", description = "새로운 회원을 등록합니다.")
@PostMapping("/register")
public ResponseEntity<ApiResponse<Void>> register(@Valid @RequestBody RegisterRequest request) {
memberService.register(request);
return ResponseEntity.ok(ApiResponse.success(null, "회원가입이 완료되었습니다."));
}
/**
* ID 중복 확인
*
* @param userId 확인할 사용자 ID
* @return 중복 여부 응답
*/
@Operation(summary = "ID 중복 확인", description = "사용자 ID의 중복 여부를 확인합니다.")
@GetMapping("/check-duplicate")
public ResponseEntity<ApiResponse<DuplicateCheckResponse>> checkDuplicate(
@Parameter(description = "확인할 사용자 ID", required = true)
@RequestParam String userId) {
boolean isDuplicate = memberService.checkDuplicate(userId);
DuplicateCheckResponse response = DuplicateCheckResponse.builder()
.isDuplicate(isDuplicate)
.message(isDuplicate ? "이미 사용 중인 ID입니다." : "사용 가능한 ID입니다.")
.build();
return ResponseEntity.ok(ApiResponse.success(response));
}
/**
* 패스워드 유효성 검증
*
* @param request 패스워드 유효성 검증 요청
* @return 유효성 검증 결과
*/
@Operation(summary = "패스워드 유효성 검증", description = "패스워드가 보안 규칙을 만족하는지 확인합니다.")
@PostMapping("/validate-password")
public ResponseEntity<ApiResponse<ValidationResponse>> validatePassword(@Valid @RequestBody PasswordValidationRequest request) {
ValidationResponse response = memberService.validatePassword(request.getPassword());
return ResponseEntity.ok(ApiResponse.success(response));
}
}
@@ -0,0 +1,25 @@
package com.won.smarketing.member.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* ID 중복 확인 응답 DTO
* 사용자 ID 중복 여부 확인 결과
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "ID 중복 확인 응답")
public class DuplicateCheckResponse {
@Schema(description = "중복 여부", example = "false")
private boolean isDuplicate;
@Schema(description = "확인 결과 메시지", example = "사용 가능한 ID입니다.")
private String message;
}
@@ -0,0 +1,29 @@
package com.won.smarketing.member.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
/**
* 로그인 요청 DTO
* 로그인 시 필요한 사용자 ID와 패스워드 정보
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "로그인 요청 정보")
public class LoginRequest {
@Schema(description = "사용자 ID", example = "testuser", required = true)
@NotBlank(message = "사용자 ID는 필수입니다.")
private String userId;
@Schema(description = "패스워드", example = "password123!", required = true)
@NotBlank(message = "패스워드는 필수입니다.")
private String password;
}
@@ -0,0 +1,28 @@
package com.won.smarketing.member.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 로그인 응답 DTO
* 로그인 성공 시 반환되는 JWT 토큰 정보
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "로그인 응답 정보")
public class LoginResponse {
@Schema(description = "Access Token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
private String accessToken;
@Schema(description = "Refresh Token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
private String refreshToken;
@Schema(description = "토큰 만료 시간 (밀리초)", example = "900000")
private long expiresIn;
}
@@ -0,0 +1,25 @@
package com.won.smarketing.member.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
/**
* 로그아웃 요청 DTO
* 로그아웃 시 무효화할 Refresh Token 정보
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "로그아웃 요청")
public class LogoutRequest {
@Schema(description = "무효화할 Refresh Token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", required = true)
@NotBlank(message = "Refresh Token은 필수입니다.")
private String refreshToken;
}
@@ -0,0 +1,25 @@
package com.won.smarketing.member.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
/**
* 패스워드 유효성 검증 요청 DTO
* 패스워드 보안 규칙 확인을 위한 요청 정보
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "패스워드 유효성 검증 요청")
public class PasswordValidationRequest {
@Schema(description = "검증할 패스워드", example = "password123!", required = true)
@NotBlank(message = "패스워드는 필수입니다.")
private String password;
}
@@ -0,0 +1,49 @@
package com.won.smarketing.member.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
/**
* 회원가입 요청 DTO
* 회원가입 시 필요한 정보를 담는 데이터 전송 객체
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "회원가입 요청 정보")
public class RegisterRequest {
@Schema(description = "사용자 ID", example = "testuser", required = true)
@NotBlank(message = "사용자 ID는 필수입니다.")
@Size(min = 4, max = 20, message = "사용자 ID는 4자 이상 20자 이하여야 합니다.")
@Pattern(regexp = "^[a-zA-Z0-9]+$", message = "사용자 ID는 영문과 숫자만 가능합니다.")
private String userId;
@Schema(description = "패스워드", example = "password123!", required = true)
@NotBlank(message = "패스워드는 필수입니다.")
private String password;
@Schema(description = "이름", example = "홍길동", required = true)
@NotBlank(message = "이름은 필수입니다.")
@Size(max = 100, message = "이름은 100자 이하여야 합니다.")
private String name;
@Schema(description = "사업자 번호", example = "123-45-67890", required = true)
@NotBlank(message = "사업자 번호는 필수입니다.")
@Pattern(regexp = "^\\d{3}-\\d{2}-\\d{5}$", message = "사업자 번호 형식이 올바르지 않습니다.")
private String businessNumber;
@Schema(description = "이메일", example = "test@example.com", required = true)
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "올바른 이메일 형식이 아닙니다.")
private String email;
}
@@ -0,0 +1,25 @@
package com.won.smarketing.member.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
/**
* 토큰 갱신 요청 DTO
* Refresh Token을 사용한 토큰 갱신 요청 정보
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "토큰 갱신 요청")
public class TokenRefreshRequest {
@Schema(description = "Refresh Token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", required = true)
@NotBlank(message = "Refresh Token은 필수입니다.")
private String refreshToken;
}
@@ -0,0 +1,28 @@
package com.won.smarketing.member.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 토큰 응답 DTO
* 토큰 갱신 시 반환되는 새로운 JWT 토큰 정보
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "토큰 응답 정보")
public class TokenResponse {
@Schema(description = "새로운 Access Token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
private String accessToken;
@Schema(description = "새로운 Refresh Token", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
private String refreshToken;
@Schema(description = "토큰 만료 시간 (밀리초)", example = "900000")
private long expiresIn;
}
@@ -0,0 +1,30 @@
package com.won.smarketing.member.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 유효성 검증 응답 DTO
* 패스워드 유효성 검증 결과 정보
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "유효성 검증 응답")
public class ValidationResponse {
@Schema(description = "유효성 여부", example = "true")
private boolean isValid;
@Schema(description = "검증 결과 메시지", example = "유효한 패스워드입니다.")
private String message;
@Schema(description = "오류 목록")
private List<String> errors;
}
@@ -0,0 +1,87 @@
package com.won.smarketing.member.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
/**
* 회원 정보를 나타내는 엔티티
* 사용자 ID, 패스워드, 이름, 사업자 번호, 이메일 정보 저장
*/
@Entity
@Table(name = "members")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Member {
/**
* 회원 고유 식별자
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 사용자 ID (로그인용)
*/
@Column(name = "user_id", unique = true, nullable = false, length = 50)
private String userId;
/**
* 암호화된 패스워드
*/
@Column(name = "password", nullable = false)
private String password;
/**
* 회원 이름
*/
@Column(name = "name", nullable = false, length = 100)
private String name;
/**
* 사업자 번호
*/
@Column(name = "business_number", unique = true, nullable = false, length = 20)
private String businessNumber;
/**
* 이메일 주소
*/
@Column(name = "email", unique = true, nullable = false)
private String email;
/**
* 회원 생성 시각
*/
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
/**
* 회원 정보 수정 시각
*/
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
/**
* 엔티티 저장 전 실행되는 메서드
* 생성 시각과 수정 시각을 현재 시각으로 설정
*/
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
/**
* 엔티티 업데이트 전 실행되는 메서드
* 수정 시각을 현재 시각으로 갱신
*/
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
@@ -0,0 +1,47 @@
package com.won.smarketing.member.repository;
import com.won.smarketing.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* 회원 정보 데이터 접근을 위한 Repository
* JPA를 사용한 회원 CRUD 작업 처리
*/
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
/**
* 사용자 ID로 회원 조회
*
* @param userId 사용자 ID
* @return 회원 정보
*/
Optional<Member> findByUserId(String userId);
/**
* 사용자 ID 존재 여부 확인
*
* @param userId 사용자 ID
* @return 존재 여부
*/
boolean existsByUserId(String userId);
/**
* 이메일 존재 여부 확인
*
* @param email 이메일
* @return 존재 여부
*/
boolean existsByEmail(String email);
/**
* 사업자 번호 존재 여부 확인
*
* @param businessNumber 사업자 번호
* @return 존재 여부
*/
boolean existsByBusinessNumber(String businessNumber);
}
@@ -0,0 +1,35 @@
package com.won.smarketing.member.service;
import com.won.smarketing.member.dto.LoginRequest;
import com.won.smarketing.member.dto.LoginResponse;
import com.won.smarketing.member.dto.TokenResponse;
/**
* 인증/인가 서비스 인터페이스
* 로그인, 로그아웃, 토큰 갱신 기능 정의
*/
public interface AuthService {
/**
* 로그인 인증 처리
*
* @param request 로그인 요청 정보
* @return JWT 토큰 정보
*/
LoginResponse login(LoginRequest request);
/**
* 로그아웃 처리
*
* @param refreshToken 무효화할 Refresh Token
*/
void logout(String refreshToken);
/**
* 토큰 갱신 처리
*
* @param refreshToken 갱신에 사용할 Refresh Token
* @return 새로운 JWT 토큰 정보
*/
TokenResponse refresh(String refreshToken);
}
@@ -0,0 +1,131 @@
package com.won.smarketing.member.service;
import com.won.smarketing.common.exception.BusinessException;
import com.won.smarketing.common.exception.ErrorCode;
import com.won.smarketing.common.security.JwtTokenProvider;
import com.won.smarketing.member.dto.LoginRequest;
import com.won.smarketing.member.dto.LoginResponse;
import com.won.smarketing.member.dto.TokenResponse;
import com.won.smarketing.member.entity.Member;
import com.won.smarketing.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
/**
* 인증/인가 서비스 구현체
* 로그인, 로그아웃, 토큰 갱신 기능 구현
*/
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthServiceImpl implements AuthService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final RedisTemplate<String, String> redisTemplate;
private static final String REFRESH_TOKEN_PREFIX = "refresh_token:";
/**
* 로그인 인증 처리
*
* @param request 로그인 요청 정보
* @return JWT 토큰 정보
*/
@Override
@Transactional
public LoginResponse login(LoginRequest request) {
// 사용자 조회
Member member = memberRepository.findByUserId(request.getUserId())
.orElseThrow(() -> new BusinessException(ErrorCode.MEMBER_NOT_FOUND));
// 패스워드 검증
if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) {
throw new BusinessException(ErrorCode.INVALID_PASSWORD);
}
// JWT 토큰 생성
String accessToken = jwtTokenProvider.generateAccessToken(member.getUserId());
String refreshToken = jwtTokenProvider.generateRefreshToken(member.getUserId());
long expiresIn = jwtTokenProvider.getAccessTokenValidityTime();
// Refresh Token을 Redis에 저장
String refreshTokenKey = REFRESH_TOKEN_PREFIX + member.getUserId();
redisTemplate.opsForValue().set(refreshTokenKey, refreshToken,
jwtTokenProvider.getRefreshTokenValidityTime(), TimeUnit.MILLISECONDS);
return LoginResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.expiresIn(expiresIn)
.build();
}
/**
* 로그아웃 처리
*
* @param refreshToken 무효화할 Refresh Token
*/
@Override
@Transactional
public void logout(String refreshToken) {
// 토큰에서 사용자 ID 추출
String userId = jwtTokenProvider.getUserIdFromToken(refreshToken);
// Redis에서 Refresh Token 삭제
String refreshTokenKey = REFRESH_TOKEN_PREFIX + userId;
redisTemplate.delete(refreshTokenKey);
}
/**
* 토큰 갱신 처리
*
* @param refreshToken 갱신에 사용할 Refresh Token
* @return 새로운 JWT 토큰 정보
*/
@Override
@Transactional
public TokenResponse refresh(String refreshToken) {
// Refresh Token 유효성 검증
if (!jwtTokenProvider.validateToken(refreshToken)) {
throw new BusinessException(ErrorCode.INVALID_TOKEN);
}
// 토큰에서 사용자 ID 추출
String userId = jwtTokenProvider.getUserIdFromToken(refreshToken);
// Redis에서 저장된 Refresh Token 확인
String refreshTokenKey = REFRESH_TOKEN_PREFIX + userId;
String storedRefreshToken = redisTemplate.opsForValue().get(refreshTokenKey);
if (storedRefreshToken == null || !storedRefreshToken.equals(refreshToken)) {
throw new BusinessException(ErrorCode.INVALID_TOKEN);
}
// 사용자 존재 여부 확인
Member member = memberRepository.findByUserId(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.MEMBER_NOT_FOUND));
// 새로운 토큰 생성
String newAccessToken = jwtTokenProvider.generateAccessToken(member.getUserId());
String newRefreshToken = jwtTokenProvider.generateRefreshToken(member.getUserId());
long expiresIn = jwtTokenProvider.getAccessTokenValidityTime();
// 기존 Refresh Token 삭제 후 새로운 토큰 저장
redisTemplate.delete(refreshTokenKey);
redisTemplate.opsForValue().set(refreshTokenKey, newRefreshToken,
jwtTokenProvider.getRefreshTokenValidityTime(), TimeUnit.MILLISECONDS);
return TokenResponse.builder()
.accessToken(newAccessToken)
.refreshToken(newRefreshToken)
.expiresIn(expiresIn)
.build();
}
}
@@ -0,0 +1,34 @@
package com.won.smarketing.member.service;
import com.won.smarketing.member.dto.RegisterRequest;
import com.won.smarketing.member.dto.ValidationResponse;
/**
* 회원 관리 서비스 인터페이스
* 회원가입, 중복 확인, 패스워드 유효성 검증 기능 정의
*/
public interface MemberService {
/**
* 회원가입 처리
*
* @param request 회원가입 요청 정보
*/
void register(RegisterRequest request);
/**
* 사용자 ID 중복 확인
*
* @param userId 확인할 사용자 ID
* @return 중복 여부 (true: 중복, false: 사용 가능)
*/
boolean checkDuplicate(String userId);
/**
* 패스워드 유효성 검증
*
* @param password 검증할 패스워드
* @return 유효성 검증 결과
*/
ValidationResponse validatePassword(String password);
}
@@ -0,0 +1,115 @@
package com.won.smarketing.member.service;
import com.won.smarketing.common.exception.BusinessException;
import com.won.smarketing.common.exception.ErrorCode;
import com.won.smarketing.member.dto.RegisterRequest;
import com.won.smarketing.member.dto.ValidationResponse;
import com.won.smarketing.member.entity.Member;
import com.won.smarketing.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* 회원 관리 서비스 구현체
* 회원가입, 중복 확인, 패스워드 유효성 검증 기능 구현
*/
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
// 패스워드 정규식: 영문, 숫자, 특수문자 각각 최소 1개 포함, 8자 이상
private static final Pattern PASSWORD_PATTERN = Pattern.compile(
"^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$"
);
/**
* 회원가입 처리
*
* @param request 회원가입 요청 정보
*/
@Override
@Transactional
public void register(RegisterRequest request) {
// 중복 ID 확인
if (memberRepository.existsByUserId(request.getUserId())) {
throw new BusinessException(ErrorCode.DUPLICATE_MEMBER_ID);
}
// 이메일 중복 확인
if (memberRepository.existsByEmail(request.getEmail())) {
throw new BusinessException(ErrorCode.DUPLICATE_EMAIL);
}
// 사업자 번호 중복 확인
if (memberRepository.existsByBusinessNumber(request.getBusinessNumber())) {
throw new BusinessException(ErrorCode.DUPLICATE_BUSINESS_NUMBER);
}
// 패스워드 암호화
String encodedPassword = passwordEncoder.encode(request.getPassword());
// 회원 엔티티 생성 및 저장
Member member = Member.builder()
.userId(request.getUserId())
.password(encodedPassword)
.name(request.getName())
.businessNumber(request.getBusinessNumber())
.email(request.getEmail())
.build();
memberRepository.save(member);
}
/**
* 사용자 ID 중복 확인
*
* @param userId 확인할 사용자 ID
* @return 중복 여부 (true: 중복, false: 사용 가능)
*/
@Override
public boolean checkDuplicate(String userId) {
return memberRepository.existsByUserId(userId);
}
/**
* 패스워드 유효성 검증
*
* @param password 검증할 패스워드
* @return 유효성 검증 결과
*/
@Override
public ValidationResponse validatePassword(String password) {
List<String> errors = new ArrayList<>();
boolean isValid = true;
// 길이 검증 (8자 이상)
if (password.length() < 8) {
errors.add("패스워드는 8자 이상이어야 합니다.");
isValid = false;
}
// 패턴 검증 (영문, 숫자, 특수문자 포함)
if (!PASSWORD_PATTERN.matcher(password).matches()) {
errors.add("패스워드는 영문, 숫자, 특수문자를 각각 최소 1개씩 포함해야 합니다.");
isValid = false;
}
String message = isValid ? "유효한 패스워드입니다." : "패스워드가 보안 규칙을 만족하지 않습니다.";
return ValidationResponse.builder()
.isValid(isValid)
.message(message)
.errors(errors)
.build();
}
}
+42
View File
@@ -0,0 +1,42 @@
server:
port: ${SERVER_PORT:8081}
servlet:
context-path: /
spring:
application:
name: member-service
datasource:
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:memberdb}
username: ${POSTGRES_USER:postgres}
password: ${POSTGRES_PASSWORD:postgres}
jpa:
hibernate:
ddl-auto: ${JPA_DDL_AUTO:update}
show-sql: ${JPA_SHOW_SQL:true}
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
timeout: 2000ms
jwt:
secret-key: ${JWT_SECRET_KEY:mySecretKeyForJWTTokenGenerationThatShouldBeVeryLongAndSecure}
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:900000}
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:86400000}
springdoc:
swagger-ui:
path: /swagger-ui.html
operations-sorter: method
api-docs:
path: /api-docs
logging:
level:
com.won.smarketing.member: ${LOG_LEVEL:DEBUG}