Skip to content

Commit 45ee2b8

Browse files
authored
Merge pull request #75 from UruruLab/feat/65-seller
판매자 회원가입 기능 구현 및 검증 로직 개선
2 parents 57094bf + c8f3cae commit 45ee2b8

File tree

11 files changed

+401
-6
lines changed

11 files changed

+401
-6
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.ururulab.ururu.global.common.util;
2+
3+
import lombok.experimental.UtilityClass;
4+
5+
/**
6+
* 민감한 정보 마스킹을 위한 유틸리티 클래스
7+
* 로그 출력 시 개인정보 보호를 위해 사용
8+
*/
9+
@UtilityClass
10+
public class MaskingUtils {
11+
12+
/**
13+
* 이메일 마스킹
14+
*/
15+
public static String maskEmail(final String email) {
16+
if (email == null || email.length() <= 3) {
17+
return "***";
18+
}
19+
final int atIndex = email.indexOf('@');
20+
if (atIndex <= 1) {
21+
return "***";
22+
}
23+
return email.substring(0, 1) + "***@" + email.substring(atIndex + 1);
24+
}
25+
26+
/**
27+
* 사업자등록번호 마스킹
28+
*/
29+
public static String maskBusinessNumber(final String businessNumber) {
30+
if (businessNumber == null || businessNumber.length() <= 7) {
31+
return "***";
32+
}
33+
return businessNumber.substring(0, 3) + "****" + businessNumber.substring(7);
34+
}
35+
36+
/**
37+
* 전화번호 마스킹
38+
*/
39+
public static String maskPhone(final String phone) {
40+
if (phone == null || phone.length() <= 7) {
41+
return "***";
42+
}
43+
return phone.substring(0, 3) + "****" + phone.substring(7);
44+
}
45+
46+
/**
47+
* 이름 마스킹
48+
*/
49+
public static String maskName(final String name) {
50+
if (name == null || name.length() <= 1) {
51+
return "***";
52+
}
53+
if (name.length() == 2) {
54+
return name.substring(0, 1) + "*";
55+
}
56+
return name.substring(0, 1) + "*" + name.substring(name.length() - 1);
57+
}
58+
}

src/main/java/com/ururulab/ururu/global/config/SecurityConfig.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,22 @@
77
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
88
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
99
import org.springframework.security.config.http.SessionCreationPolicy;
10+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
11+
import org.springframework.security.crypto.password.PasswordEncoder;
1012
import org.springframework.security.web.SecurityFilterChain;
1113

1214
@Configuration
1315
@EnableWebSecurity
1416
public class SecurityConfig {
1517

18+
/**
19+
* 비밀번호 암호화를 위한 PasswordEncoder 빈
20+
*/
21+
@Bean
22+
public PasswordEncoder passwordEncoder() {
23+
return new BCryptPasswordEncoder();
24+
}
25+
1626
/**
1727
* 개발 환경용 Security 설정
1828
* - 모든 요청 허용 (개발 편의성 우선)

src/main/java/com/ururulab/ururu/global/exception/GlobalExceptionHandler.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import io.jsonwebtoken.ExpiredJwtException;
1313
import io.jsonwebtoken.MalformedJwtException;
1414
import lombok.extern.slf4j.Slf4j;
15+
import org.springframework.dao.DataIntegrityViolationException;
1516
import org.springframework.http.HttpStatus;
1617
import org.springframework.http.ResponseEntity;
1718
import org.springframework.validation.FieldError;
@@ -100,6 +101,19 @@ public ResponseEntity<ApiResponseFormat<Void>> handleValidation(
100101
.body(ApiResponseFormat.fail(errorMessage));
101102
}
102103

104+
/**
105+
* DB 무결성 제약조건 위반 예외 처리.
106+
*/
107+
@ExceptionHandler(DataIntegrityViolationException.class)
108+
public ResponseEntity<ApiResponseFormat<Void>> handleDataIntegrityViolation(
109+
final DataIntegrityViolationException exception
110+
) {
111+
log.warn("Data integrity violation: {}", exception.getMessage());
112+
return ResponseEntity
113+
.status(HttpStatus.CONFLICT)
114+
.body(ApiResponseFormat.fail("이미 사용 중인 정보입니다."));
115+
}
116+
103117
/**
104118
* IllegalArgumentException 처리.
105119
*/
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.ururulab.ururu.seller.controller;
2+
3+
import com.ururulab.ururu.global.common.dto.ApiResponseFormat;
4+
import com.ururulab.ururu.global.common.util.MaskingUtils;
5+
import com.ururulab.ururu.seller.domain.dto.request.SellerSignupRequest;
6+
import com.ururulab.ururu.seller.domain.dto.response.SellerAvailabilityResponse;
7+
import com.ururulab.ururu.seller.domain.dto.response.SellerResponse;
8+
import com.ururulab.ururu.seller.domain.dto.response.SellerSignupResponse;
9+
import com.ururulab.ururu.seller.service.SellerService;
10+
import jakarta.validation.Valid;
11+
import lombok.RequiredArgsConstructor;
12+
import lombok.extern.slf4j.Slf4j;
13+
import org.springframework.http.HttpStatus;
14+
import org.springframework.http.ResponseEntity;
15+
import org.springframework.web.bind.annotation.*;
16+
17+
@Slf4j
18+
@RestController
19+
@RequestMapping("/api/sellers")
20+
@RequiredArgsConstructor
21+
public class SellerController {
22+
23+
private final SellerService sellerService;
24+
25+
/**
26+
* 판매자 회원가입
27+
* @param request 판매자 회원가입 요청
28+
* @return 회원가입 결과
29+
*/
30+
@PostMapping("/signup")
31+
public ResponseEntity<ApiResponseFormat<SellerSignupResponse>> signup(
32+
@Valid @RequestBody final SellerSignupRequest request
33+
) {
34+
final SellerSignupResponse response = sellerService.signup(request);
35+
log.info("판매자 회원가입 API 호출 성공: ID={}", response.id());
36+
37+
return ResponseEntity.status(HttpStatus.CREATED)
38+
.body(ApiResponseFormat.success("판매자 회원가입이 완료되었습니다.", response));
39+
}
40+
41+
/**
42+
* 이메일 중복 체크
43+
* @param email 체크할 이메일
44+
* @return 가용성 결과
45+
*/
46+
@GetMapping("/check/email")
47+
public ResponseEntity<ApiResponseFormat<SellerAvailabilityResponse>> checkEmailAvailability(
48+
@RequestParam final String email
49+
) {
50+
final SellerAvailabilityResponse response = sellerService.checkEmailAvailability(email);
51+
log.debug("이메일 중복 체크 API 호출: {}", MaskingUtils.maskEmail(email));
52+
53+
return ResponseEntity.ok(
54+
ApiResponseFormat.success("이메일 가용성 체크가 완료되었습니다.", response));
55+
}
56+
57+
/**
58+
* 사업자등록번호 중복 체크
59+
* @param businessNumber 체크할 사업자등록번호
60+
* @return 가용성 결과
61+
*/
62+
@GetMapping("/check/business-number")
63+
public ResponseEntity<ApiResponseFormat<SellerAvailabilityResponse>> checkBusinessNumberAvailability(
64+
@RequestParam final String businessNumber
65+
) {
66+
final SellerAvailabilityResponse response = sellerService.checkBusinessNumberAvailability(businessNumber);
67+
log.debug("사업자등록번호 중복 체크 API 호출: {}", MaskingUtils.maskBusinessNumber(businessNumber));
68+
69+
return ResponseEntity.ok(
70+
ApiResponseFormat.success("사업자등록번호 가용성 체크가 완료되었습니다.", response));
71+
}
72+
73+
/**
74+
* 브랜드명 중복 체크
75+
* @param name 체크할 브랜드명
76+
* @return 가용성 결과
77+
*/
78+
@GetMapping("/check/name")
79+
public ResponseEntity<ApiResponseFormat<SellerAvailabilityResponse>> checkNameAvailability(
80+
@RequestParam final String name
81+
) {
82+
final SellerAvailabilityResponse response = sellerService.checkNameAvailability(name);
83+
log.debug("브랜드명 중복 체크 API 호출: {}", name);
84+
85+
return ResponseEntity.ok(
86+
ApiResponseFormat.success("브랜드명 가용성 체크가 완료되었습니다.", response));
87+
}
88+
89+
/**
90+
* 판매자 프로필 조회
91+
* @param sellerId 판매자 ID
92+
* @return 판매자 정보
93+
*/
94+
@GetMapping("/{sellerId}")
95+
public ResponseEntity<ApiResponseFormat<SellerResponse>> getSellerProfile(
96+
@PathVariable final Long sellerId
97+
) {
98+
final SellerResponse response = sellerService.getSellerProfile(sellerId);
99+
log.debug("판매자 프로필 조회 API 호출: ID={}", sellerId);
100+
101+
return ResponseEntity.ok(
102+
ApiResponseFormat.success("판매자 정보 조회가 완료되었습니다.", response));
103+
}
104+
}

src/main/java/com/ururulab/ururu/seller/domain/dto/request/SellerRequest.java renamed to src/main/java/com/ururulab/ururu/seller/domain/dto/request/SellerSignupRequest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import com.ururulab.ururu.seller.domain.dto.validation.SellerValidationPatterns;
66
import jakarta.validation.constraints.*;
77

8-
public record SellerRequest(
8+
public record SellerSignupRequest(
99
@NotBlank(message = SellerValidationMessages.NAME_REQUIRED)
1010
@Size(max = SellerValidationConstants.NAME_MAX_LENGTH,
1111
message = SellerValidationMessages.NAME_SIZE)
@@ -64,4 +64,4 @@ public record SellerRequest(
6464
message = SellerValidationMessages.MAIL_ORDER_NUMBER_SIZE)
6565
String mailOrderNumber
6666
) {
67-
}
67+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.ururulab.ururu.seller.domain.dto.response;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import com.ururulab.ururu.seller.domain.entity.Seller;
5+
6+
import java.time.ZonedDateTime;
7+
8+
// 판매자 회원가입 응답 DTO
9+
public record SellerSignupResponse(
10+
Long id,
11+
String name,
12+
@JsonProperty("business_name") String businessName,
13+
@JsonProperty("owner_name") String ownerName,
14+
@JsonProperty("business_number") String businessNumber,
15+
String email,
16+
String phone,
17+
String image,
18+
String address1,
19+
String address2,
20+
@JsonProperty("mail_order_number") String mailOrderNumber,
21+
@JsonProperty("created_at") ZonedDateTime createdAt,
22+
@JsonProperty("updated_at") ZonedDateTime updatedAt
23+
// password 필드는 보안상 응답에서 제외 (민감한 인증 정보)
24+
) {
25+
public static SellerSignupResponse of(final Seller seller) {
26+
if (seller == null) {
27+
throw new IllegalArgumentException("Seller는 필수입니다.");
28+
}
29+
30+
return new SellerSignupResponse(
31+
seller.getId(),
32+
seller.getName(),
33+
seller.getBusinessName(),
34+
seller.getOwnerName(),
35+
seller.getBusinessNumber(),
36+
seller.getEmail(),
37+
seller.getPhone(),
38+
seller.getImage(),
39+
seller.getAddress1(),
40+
seller.getAddress2(),
41+
seller.getMailOrderNumber(),
42+
seller.getCreatedAt(),
43+
seller.getUpdatedAt()
44+
);
45+
}
46+
}

src/main/java/com/ururulab/ururu/seller/domain/dto/validation/SellerValidationMessages.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public class SellerValidationMessages {
2727
SellerValidationConstants.EMAIL_MAX_LENGTH + "자 이하여야 합니다.";
2828

2929
public static final String PASSWORD_REQUIRED = "비밀번호는 필수입니다.";
30+
public static final String PASSWORD_SIZE = "비밀번호는 " +
31+
SellerValidationConstants.PASSWORD_MAX_LENGTH + "자 이하여야 합니다.";
3032
public static final String PASSWORD_PATTERN = "비밀번호는 영문, 숫자, 특수문자를 포함하여 8자 이상이어야 합니다.";
3133

3234
public static final String PHONE_REQUIRED = "전화번호는 필수입니다.";

src/main/java/com/ururulab/ururu/seller/domain/dto/validation/SellerValidationPatterns.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ public class SellerValidationPatterns {
77
public static final String BUSINESS_NUMBER_PATTERN = "^[0-9]{10}$";
88
public static final String PASSWORD_PATTERN = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,}$";
99
public static final String PHONE_PATTERN = "^[0-9]{10,11}$";
10+
public static final String EMAIL_PATTERN = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
1011
}

src/main/java/com/ururulab/ururu/seller/domain/entity/Seller.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,10 @@ public static Seller of(
9090
seller.businessNumber = businessNumber.trim();
9191
seller.email = email.trim();
9292
seller.password = password; // 암호화는 Service 레이어에서 처리
93-
seller.phone = phone;
93+
seller.phone = phone.trim();
9494
seller.image = image;
9595
seller.address1 = address1.trim();
96-
seller.address2 = address2 != null ? address2.trim() : "";
96+
seller.address2 = address2.trim();
9797
seller.mailOrderNumber = mailOrderNumber.trim();
9898
return seller;
9999
}
@@ -115,7 +115,7 @@ public void updateOwnerName(final String ownerName) {
115115

116116
public void updatePhone(final String phone) {
117117
SellerPolicy.validatePhone(phone);
118-
this.phone = phone;
118+
this.phone = phone.trim();
119119
}
120120

121121
public void updateImage(final String image) {

src/main/java/com/ururulab/ururu/seller/domain/policy/SellerPolicy.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.ururulab.ururu.seller.domain.policy;
22

3+
import com.ururulab.ururu.seller.domain.dto.validation.SellerValidationConstants;
34
import com.ururulab.ururu.seller.domain.dto.validation.SellerValidationMessages;
45
import com.ururulab.ururu.seller.domain.dto.validation.SellerValidationPatterns;
56
import lombok.experimental.UtilityClass;
@@ -44,7 +45,10 @@ public static void validateEmail(String email) {
4445
throw new IllegalArgumentException(SellerValidationMessages.EMAIL_REQUIRED);
4546
}
4647

47-
if (!email.trim().contains("@")) {
48+
// 이메일 정규화 (소문자 변환)
49+
final String normalizedEmail = email.trim().toLowerCase();
50+
51+
if (!normalizedEmail.matches(SellerValidationPatterns.EMAIL_PATTERN)) {
4852
throw new IllegalArgumentException(SellerValidationMessages.EMAIL_FORMAT);
4953
}
5054
}
@@ -54,6 +58,9 @@ public static void validatePassword(String password) {
5458
if (password == null || password.trim().isEmpty()) {
5559
throw new IllegalArgumentException(SellerValidationMessages.PASSWORD_REQUIRED);
5660
}
61+
if (password.length() > SellerValidationConstants.PASSWORD_MAX_LENGTH) {
62+
throw new IllegalArgumentException(SellerValidationMessages.PASSWORD_SIZE);
63+
}
5764
if (!password.matches(SellerValidationPatterns.PASSWORD_PATTERN)) {
5865
throw new IllegalArgumentException(SellerValidationMessages.PASSWORD_PATTERN);
5966
}

0 commit comments

Comments
 (0)