From 56d83bcaeb46cf55961312c25c08fc43895584ba Mon Sep 17 00:00:00 2001 From: Hye_rin Mun Date: Tue, 14 Apr 2026 20:09:38 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20User=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=B0=8F=20=EB=82=B4=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/auth/service/AuthService.java | 4 +- .../common/util/MaskingUtils.java | 75 +++++++++++++++++++ .../user/controller/UserController.java | 40 ++++++++++ .../user/dto/request/UpdatemeUserRequest.java | 17 +++++ .../user/dto/response/GetmeUserResponse.java | 26 +++++++ .../dto/response/UpdatemeUserResponse.java | 21 ++++++ .../domain/user/entity/User.java | 7 ++ .../user/repository/UserRepository.java | 3 + .../user/service/UserCommandService.java | 4 + .../user/service/UserCommandServiceImpl.java | 20 +++++ .../domain/user/service/UserQueryService.java | 7 +- .../user/service/UserQueryServiceImpl.java | 21 +++++- .../domain/user/service/UserService.java | 15 ---- 13 files changed, 240 insertions(+), 20 deletions(-) create mode 100644 src/main/java/jpa/basic/alldayprojectcommerce/common/util/MaskingUtils.java create mode 100644 src/main/java/jpa/basic/alldayprojectcommerce/domain/user/controller/UserController.java create mode 100644 src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatemeUserRequest.java create mode 100644 src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/response/GetmeUserResponse.java create mode 100644 src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/response/UpdatemeUserResponse.java delete mode 100644 src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserService.java diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/common/security/auth/service/AuthService.java b/src/main/java/jpa/basic/alldayprojectcommerce/common/security/auth/service/AuthService.java index 3c6188d..882caac 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/common/security/auth/service/AuthService.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/common/security/auth/service/AuthService.java @@ -3,6 +3,7 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jpa.basic.alldayprojectcommerce.common.exception.CustomException; import jpa.basic.alldayprojectcommerce.common.exception.ErrorCode; import jpa.basic.alldayprojectcommerce.common.security.auth.AuthConstants; import jpa.basic.alldayprojectcommerce.common.security.auth.AuthTokens; @@ -50,7 +51,8 @@ public void signup(CreateUserRequest request, HttpServletResponse response) { } public void login(LoginUserRequest request, HttpServletResponse response) { - User user = userQueryService.getByEmail(request.email()); + User user = userQueryService.getByEmail(request.email()) + .orElseThrow(() -> new CustomException(ErrorCode.AUTH_USER_NOT_FOUND)); if (!passwordEncoder.matches(request.password(), user.getPassword())) { throw new AuthUnauthenticatedException(ErrorCode.AUTH_UNAUTHENTICATED_USER); diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/common/util/MaskingUtils.java b/src/main/java/jpa/basic/alldayprojectcommerce/common/util/MaskingUtils.java new file mode 100644 index 0000000..cab44cb --- /dev/null +++ b/src/main/java/jpa/basic/alldayprojectcommerce/common/util/MaskingUtils.java @@ -0,0 +1,75 @@ +package jpa.basic.alldayprojectcommerce.common.util; + +public class MaskingUtils { + private MaskingUtils() {} + + /* + * 이메일 마스킹 + * "test@test.com -> "te**@test.com + * */ + public static String maskEmail(String email) { + if (email == null || email.isBlank()) return null; + int atIdx = email.indexOf('@'); + if(atIdx <= 0) return email; + + String local = email.substring(0, atIdx); + String domain = email.substring(atIdx); // "@test.com" + + if (local.length() <= 2) return email; // 너무 짧으면 그대로 출력 + + String exposed = local.substring(0, 2); + String masked = "*".repeat(local.length() -2); + return exposed + masked + domain; + } + + /* + * 이름 마스킹 - 첫글자 노출, 나머지 * + * "홍길동" -> "홍*동" + * "김사무엘" -> "김***" + * "이도" -> "이* + */ + public static String maskName(String name) { + if(name == null || name.isBlank()) return null; + int len = name.length(); + if (len == 1) return name; + + return name.charAt(0) + "*".repeat(len - 1); + } + + /* + * 비밀번호 마시킹 - 항상 고정 8자리 * + */ + public static String maskPassword() { + return "●●●●●●●●"; + } + + /* + * 전화번호 마스킹 - 가운데 4자리 마스킹 + * "010-1234-1234" -> "010-****-1234" + */ + public static String maskPhone(String phone) { + if(phone == null || phone.isBlank()) return null; + + phone = phone.replace("-", ""); + if(phone.matches("\\d{11}")) { + return phone.substring(0, 3) + "****" + phone.substring(7); + } + + return phone; //예상치 못한 형식은 그대로 + } + + /* + * 주소 마스킹 - 공백 기준 3번째부터 마스킹 + * "서울시 강남구 역삼동 123-4" -> "서울시 강남구 ***" + */ + public static String maskAddress(String address) { + if (address == null || address.isBlank()) return null; + + String[] tokens = address.split(" "); + if (tokens.length <= 2) return address; // 시,구까지만 있으면 그대로 + + // 앞 2토큰(시, 구)만 노출 + return tokens[0] + " " + tokens[1] + " ***"; + } + +} diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/controller/UserController.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/controller/UserController.java new file mode 100644 index 0000000..c0c6afb --- /dev/null +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/controller/UserController.java @@ -0,0 +1,40 @@ +package jpa.basic.alldayprojectcommerce.domain.user.controller; + +import jakarta.validation.Valid; +import jpa.basic.alldayprojectcommerce.common.ApiResponse; +import jpa.basic.alldayprojectcommerce.common.security.auth.LoginUser; +import jpa.basic.alldayprojectcommerce.common.security.auth.LoginUserInfoDto; +import jpa.basic.alldayprojectcommerce.domain.user.dto.request.UpdatemeUserRequest; +import jpa.basic.alldayprojectcommerce.domain.user.dto.response.GetmeUserResponse; +import jpa.basic.alldayprojectcommerce.domain.user.service.UserCommandService; +import jpa.basic.alldayprojectcommerce.domain.user.service.UserQueryService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/users") +@RequiredArgsConstructor +public class UserController { + + private final UserQueryService userQueryService; + private final UserCommandService userCommandService; + + // 마이페이지 조회 + @GetMapping("/me") + public ResponseEntity> getMyProfile(@LoginUser LoginUserInfoDto loginUser) { + GetmeUserResponse response = userQueryService.getProfile(loginUser.id()); + return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK, response)); + } + + // 내 정보 수정 + @PatchMapping("/me") + public ResponseEntity> updateMyProfile(@LoginUser LoginUserInfoDto loginUser, @RequestBody @Valid UpdatemeUserRequest request) { + userCommandService.updateProfile(loginUser.id(), request); + return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK)); + } + + + +} diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatemeUserRequest.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatemeUserRequest.java new file mode 100644 index 0000000..29e6077 --- /dev/null +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatemeUserRequest.java @@ -0,0 +1,17 @@ +package jpa.basic.alldayprojectcommerce.domain.user.dto.request; + +import jakarta.validation.constraints.Size; + +public record UpdatemeUserRequest( + + @Size(max = 20, message = "이름은 최대 20자 입니다.") + String name, + + @Size(max = 100, message = "전화번호는 최대 100자 입니다.") + String phone, + + String address, + + //@NotBlank.. + @Size(min = 8, max = 20, message = "새 비밀번호는 최소 8자 ~ 20자입니다.") + String password) {} diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/response/GetmeUserResponse.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/response/GetmeUserResponse.java new file mode 100644 index 0000000..1309d05 --- /dev/null +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/response/GetmeUserResponse.java @@ -0,0 +1,26 @@ +package jpa.basic.alldayprojectcommerce.domain.user.dto.response; + +import jpa.basic.alldayprojectcommerce.common.util.MaskingUtils; +import jpa.basic.alldayprojectcommerce.domain.user.entity.User; +import lombok.Builder; + +@Builder +public record GetmeUserResponse( + Long id, + String email, + String name, + String password, + String phone, + String address +) { + public static GetmeUserResponse from(User user) { + return new GetmeUserResponse( + user.getId(), + MaskingUtils.maskEmail(user.getEmail()), + MaskingUtils.maskName(user.getName()), + MaskingUtils.maskPassword(), + MaskingUtils.maskPhone(user.getPhone()), + MaskingUtils.maskAddress(user.getAddress()) + ); + } +} diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/response/UpdatemeUserResponse.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/response/UpdatemeUserResponse.java new file mode 100644 index 0000000..718ec13 --- /dev/null +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/response/UpdatemeUserResponse.java @@ -0,0 +1,21 @@ +package jpa.basic.alldayprojectcommerce.domain.user.dto.response; + +import jpa.basic.alldayprojectcommerce.domain.user.entity.User; + +public record UpdatemeUserResponse( + Long id, + String email, + String name, + String phone, + String address) { + + public static UpdatemeUserResponse from(User user) { + return new UpdatemeUserResponse( + user.getId(), + user.getEmail(), + user.getName(), + user.getPhone(), + user.getAddress() + ); + } +} diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/entity/User.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/entity/User.java index d75580e..a71f9eb 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/entity/User.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/entity/User.java @@ -41,4 +41,11 @@ public static User createUser(String email, String encodedPassword) { user.password = encodedPassword; return user; } + + public void updateProfile(String name, String encodedPassword, String phone, String address) { + if(name != null) this.name = name; + this.password = encodedPassword; + if(phone != null)this.phone = phone; + if(address != null)this.address = address; + } } diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/repository/UserRepository.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/repository/UserRepository.java index 21d5c53..5213146 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/repository/UserRepository.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/repository/UserRepository.java @@ -3,6 +3,7 @@ import jpa.basic.alldayprojectcommerce.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface UserRepository extends JpaRepository { @@ -10,4 +11,6 @@ public interface UserRepository extends JpaRepository { boolean existsUserByEmail(String email); Optional findByEmail(String email); + + List id(Long id); } diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandService.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandService.java index 59404ac..4b8b710 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandService.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandService.java @@ -1,8 +1,12 @@ package jpa.basic.alldayprojectcommerce.domain.user.service; +import jpa.basic.alldayprojectcommerce.domain.user.dto.request.UpdatemeUserRequest; import jpa.basic.alldayprojectcommerce.domain.user.entity.User; public interface UserCommandService { User create(String email, String encodedPassword); + + // 내 정보 수정 + void updateProfile(Long userId, UpdatemeUserRequest request); } diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandServiceImpl.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandServiceImpl.java index 5259510..df87d5a 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandServiceImpl.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandServiceImpl.java @@ -2,9 +2,11 @@ import jpa.basic.alldayprojectcommerce.common.exception.CustomException; import jpa.basic.alldayprojectcommerce.common.exception.ErrorCode; +import jpa.basic.alldayprojectcommerce.domain.user.dto.request.UpdatemeUserRequest; import jpa.basic.alldayprojectcommerce.domain.user.entity.User; import jpa.basic.alldayprojectcommerce.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,7 +16,9 @@ public class UserCommandServiceImpl implements UserCommandService { private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + // 회원가입 @Override public User create(String email, String encodedPassword) { if (userRepository.existsUserByEmail(email)) { @@ -22,5 +26,21 @@ public User create(String email, String encodedPassword) { } return userRepository.save(User.createUser(email, encodedPassword)); } + + // 내 정보 수정 + @Override + public void updateProfile(Long userId, UpdatemeUserRequest request) { + User user = userRepository.findById(userId) + .orElseThrow( () -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // 이름, 전화번호, 주소 수정 + user.updateProfile(request.name(), request.password(), request.phone(), request.address()); + +// // 비밀번호 수정 (임시 비밀번호..) +// if(request.password() != null && !request.password().isBlank()) { +// user.updatePassword(passwordEncoder.encode(request.password())); +// } + // @Transactional에 의해 dirty-checking으로 자동 저장됨. + } } diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserQueryService.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserQueryService.java index 9282b5c..5873a02 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserQueryService.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserQueryService.java @@ -1,10 +1,15 @@ package jpa.basic.alldayprojectcommerce.domain.user.service; +import jpa.basic.alldayprojectcommerce.domain.user.dto.response.GetmeUserResponse; import jpa.basic.alldayprojectcommerce.domain.user.entity.User; +import java.util.Optional; + public interface UserQueryService { - User getByEmail(String email); + Optional getByEmail(String email); User getById(Long userId); + + GetmeUserResponse getProfile(Long userId); } diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserQueryServiceImpl.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserQueryServiceImpl.java index 9da35de..3231b03 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserQueryServiceImpl.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserQueryServiceImpl.java @@ -2,12 +2,15 @@ import jpa.basic.alldayprojectcommerce.common.exception.CustomException; import jpa.basic.alldayprojectcommerce.common.exception.ErrorCode; +import jpa.basic.alldayprojectcommerce.domain.user.dto.response.GetmeUserResponse; import jpa.basic.alldayprojectcommerce.domain.user.entity.User; import jpa.basic.alldayprojectcommerce.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @Service @Transactional(readOnly = true) @RequiredArgsConstructor @@ -15,15 +18,27 @@ public class UserQueryServiceImpl implements UserQueryService { private final UserRepository userRepository; + /** + * 인증/보안에서 로그인 기능 수행 중 사용 + * 계정 존재 여부를 가리기 위해 예외 책임을 인증/보안으로 전가 + */ @Override - public User getByEmail(String email) { - return userRepository.findByEmail(email) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + public Optional getByEmail(String email) { + return userRepository.findByEmail(email); } + /** + * Refresh Token을 발급하기 위한 User 객체 조회 + */ @Override public User getById(Long userId) { return userRepository.findById(userId) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); } + + @Override + public GetmeUserResponse getProfile(Long userId) { + User user = getById(userId); + return GetmeUserResponse.from(user); + } } diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserService.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserService.java deleted file mode 100644 index 2a80d81..0000000 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserService.java +++ /dev/null @@ -1,15 +0,0 @@ -package jpa.basic.alldayprojectcommerce.domain.user.service; - -import jpa.basic.alldayprojectcommerce.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@Transactional(readOnly = true) -@RequiredArgsConstructor -public class UserService { - - private final UserRepository userRepository; - -} From 0dc159463ad81a7c01497f516764fbebaf101bb2 Mon Sep 17 00:00:00 2001 From: Hye_rin Mun Date: Tue, 14 Apr 2026 22:41:59 +0900 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20AI=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=82=B4=EC=9A=A9=20=EB=B0=98=EC=98=81=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/util/MaskingUtils.java | 10 ++++----- .../user/dto/request/UpdatemeUserRequest.java | 10 ++++++++- .../dto/response/UpdatemeUserResponse.java | 21 ------------------- .../domain/user/entity/User.java | 2 +- 4 files changed, 15 insertions(+), 28 deletions(-) delete mode 100644 src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/response/UpdatemeUserResponse.java diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/common/util/MaskingUtils.java b/src/main/java/jpa/basic/alldayprojectcommerce/common/util/MaskingUtils.java index cab44cb..f70ef9a 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/common/util/MaskingUtils.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/common/util/MaskingUtils.java @@ -24,7 +24,7 @@ public static String maskEmail(String email) { /* * 이름 마스킹 - 첫글자 노출, 나머지 * - * "홍길동" -> "홍*동" + * "홍길동" -> "홍**" * "김사무엘" -> "김***" * "이도" -> "이* */ @@ -37,7 +37,7 @@ public static String maskName(String name) { } /* - * 비밀번호 마시킹 - 항상 고정 8자리 * + * 비밀번호 마스킹 - 항상 고정 8자리 * */ public static String maskPassword() { return "●●●●●●●●"; @@ -50,9 +50,9 @@ public static String maskPassword() { public static String maskPhone(String phone) { if(phone == null || phone.isBlank()) return null; - phone = phone.replace("-", ""); - if(phone.matches("\\d{11}")) { - return phone.substring(0, 3) + "****" + phone.substring(7); + if (phone.matches("\\d{3}-\\d{3,4}-\\d{4}")) { + String[] parts = phone.split("-"); + return parts[0] + "-****-" + parts[2]; } return phone; //예상치 못한 형식은 그대로 diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatemeUserRequest.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatemeUserRequest.java index 29e6077..d4f8423 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatemeUserRequest.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatemeUserRequest.java @@ -1,5 +1,8 @@ package jpa.basic.alldayprojectcommerce.domain.user.dto.request; +import jakarta.persistence.Column; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; public record UpdatemeUserRequest( @@ -8,10 +11,15 @@ public record UpdatemeUserRequest( String name, @Size(max = 100, message = "전화번호는 최대 100자 입니다.") + @Pattern( + regexp = "^01(?:0|1|[6-9])-(?:\\d{3}|\\d{4})-\\d{4}$", + message = "휴대폰 번호 형식이 올바르지 않습니다. (예: 010-1234-5678)" + ) String phone, + @Size(max = 255) String address, - //@NotBlank.. + //@NotBlank(message = "비밀번호는 필수 값입니다.") @Size(min = 8, max = 20, message = "새 비밀번호는 최소 8자 ~ 20자입니다.") String password) {} diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/response/UpdatemeUserResponse.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/response/UpdatemeUserResponse.java deleted file mode 100644 index 718ec13..0000000 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/response/UpdatemeUserResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package jpa.basic.alldayprojectcommerce.domain.user.dto.response; - -import jpa.basic.alldayprojectcommerce.domain.user.entity.User; - -public record UpdatemeUserResponse( - Long id, - String email, - String name, - String phone, - String address) { - - public static UpdatemeUserResponse from(User user) { - return new UpdatemeUserResponse( - user.getId(), - user.getEmail(), - user.getName(), - user.getPhone(), - user.getAddress() - ); - } -} diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/entity/User.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/entity/User.java index a71f9eb..b888805 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/entity/User.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/entity/User.java @@ -44,7 +44,7 @@ public static User createUser(String email, String encodedPassword) { public void updateProfile(String name, String encodedPassword, String phone, String address) { if(name != null) this.name = name; - this.password = encodedPassword; + if(encodedPassword != null) this.password = encodedPassword; if(phone != null)this.phone = phone; if(address != null)this.address = address; } From 3856eb49de921a318e6824abdec7510a508aaebe Mon Sep 17 00:00:00 2001 From: Hye_rin Mun Date: Fri, 17 Apr 2026 01:23:11 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=88=98=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/ErrorCode.java | 21 +++++----- .../security/LoginUserArgumentResolver.java | 2 +- .../service/OrderCommandServiceImpl.java | 2 +- .../user/controller/UserController.java | 18 +++++++-- .../dto/request/UpdatePasswordRequest.java | 14 +++++++ .../user/dto/request/UpdatemeUserRequest.java | 10 +---- .../domain/user/entity/User.java | 11 +++++- .../user/repository/UserRepository.java | 3 ++ .../user/service/UserCommandService.java | 4 ++ .../user/service/UserCommandServiceImpl.java | 38 +++++++++---------- 10 files changed, 75 insertions(+), 48 deletions(-) create mode 100644 src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatePasswordRequest.java diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/common/exception/ErrorCode.java b/src/main/java/jpa/basic/alldayprojectcommerce/common/exception/ErrorCode.java index a4f3ba7..aad093a 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/common/exception/ErrorCode.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/common/exception/ErrorCode.java @@ -12,7 +12,7 @@ public enum ErrorCode { AUTH_INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "R003", "유효하지 않은 Refresh Token 입니다."), // 권한 에러 코드 (E###) - UNAUTHORIZED_ACCESS(HttpStatus.UNAUTHORIZED, "E001", "접근 권한이 없습니다."), + FORBIDDEN_ACCESS(HttpStatus.FORBIDDEN, "E001", "접근 권한이 없습니다."), // 공통 에러 코드 (A###) INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "A001", "입력값이 올바르지 않습니다."), @@ -21,9 +21,8 @@ public enum ErrorCode { // 사용자 관련 에러 코드 (U###) USER_ALREADY_EXISTS(HttpStatus.CONFLICT, "U001", "이미 존재하는 이메일입니다."), USER_NOT_FOUND(HttpStatus.NOT_FOUND, "U002", "해당 유저는 존재하지 않습니다."), - - // 프로필 관련 에러 코드 (P###) - PROFILE_NOT_FOUND(HttpStatus.NOT_FOUND, "P001", "프로필을 찾을 수 없습니다."), + USER_PASSWORD_NOT_MATCH(HttpStatus.UNAUTHORIZED, "U003", "현재 비밀번호가 일치하지 않습니다."), + USER_PASSWORD_SAME_AS_CURRENT(HttpStatus.BAD_REQUEST, "U004", "현재 비밀번호와 동일한 비밀번호로 변경할 수 없습니다."), // 주문 관련 에러 코드 (O###) ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "O001", "주문을 찾을 수 없습니다."), @@ -31,14 +30,12 @@ public enum ErrorCode { ORDER_INVALID_STATUS(HttpStatus.BAD_REQUEST, "O003", "현재 주문 상태에서는 해당 작업을 수행할 수 없습니다."), ORDER_USER_INFO_REQUIRED(HttpStatus.BAD_REQUEST, "O004", "이름, 전화번호, 주소를 입력해주세요."), - // 상품 관련 에러 코드(A###) - PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "PR001", "해당 상품은 존재하지 않습니다."), - STOCK_LOCK_FAILED(HttpStatus.CONFLICT, "PR002", "현재 요청이 많아 처리가 지연되고 있습니다. 잠시 후 다시 시도해 주세요."), - PRODUCT_SOLD_OUT(HttpStatus.CONFLICT, "PR003", "품절된 상품입니다."), - OUT_OF_STOCK(HttpStatus.CONFLICT, "PR004", "재고가 부족합니다."), - PRODUCT_NOT_ON_SALE(HttpStatus.BAD_REQUEST, "PR005", "현재 판매 중인 상품이 아닙니다."); - - + // 상품 관련 에러 코드(P###) + PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "P001", "해당 상품은 존재하지 않습니다."), + PRODUCT_STOCK_LOCK_FAILED(HttpStatus.CONFLICT, "P002", "현재 요청이 많아 처리가 지연되고 있습니다. 잠시 후 다시 시도해 주세요."), + PRODUCT_SOLD_OUT(HttpStatus.CONFLICT, "P003", "품절된 상품입니다."), + PRODUCT_OUT_OF_STOCK(HttpStatus.CONFLICT, "P004", "재고가 부족합니다."), + PRODUCT_NOT_ON_SALE(HttpStatus.BAD_REQUEST, "P005", "현재 판매 중인 상품이 아닙니다."); private final HttpStatus status; private final String code; diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/common/security/LoginUserArgumentResolver.java b/src/main/java/jpa/basic/alldayprojectcommerce/common/security/LoginUserArgumentResolver.java index 1ad4f18..9c9dfb7 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/common/security/LoginUserArgumentResolver.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/common/security/LoginUserArgumentResolver.java @@ -38,7 +38,7 @@ public Object resolveArgument(@NonNull MethodParameter parameter, if (authentication == null || authentication.getPrincipal().equals("anonymousUser") || !(authentication.getPrincipal() instanceof LoginUserInfo)) { - throw new CustomException(ErrorCode.UNAUTHORIZED_ACCESS); + throw new CustomException(ErrorCode.FORBIDDEN_ACCESS); } // JwtAuthenticationFilter에서 Principal에 저장한 LoginUserInfo 객체를 그대로 반환 diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/order/service/OrderCommandServiceImpl.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/order/service/OrderCommandServiceImpl.java index b25af04..869ff47 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/order/service/OrderCommandServiceImpl.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/order/service/OrderCommandServiceImpl.java @@ -61,7 +61,7 @@ public CreateOrderResponse createOrder(LoginUserInfo loginUserInfo, CreateOrderR // 재고 확인 if (product.getStock() < item.quantity()) { - throw new CustomException(ErrorCode.OUT_OF_STOCK); + throw new CustomException(ErrorCode.PRODUCT_OUT_OF_STOCK); } totalAmount += product.getPrice() * item.quantity(); diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/controller/UserController.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/controller/UserController.java index b04f38f..833135c 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/controller/UserController.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/controller/UserController.java @@ -4,6 +4,7 @@ import jpa.basic.alldayprojectcommerce.common.ApiResponse; import jpa.basic.alldayprojectcommerce.common.security.auth.LoginUser; import jpa.basic.alldayprojectcommerce.common.security.auth.LoginUserInfo; +import jpa.basic.alldayprojectcommerce.domain.user.dto.request.UpdatePasswordRequest; import jpa.basic.alldayprojectcommerce.domain.user.dto.request.UpdatemeUserRequest; import jpa.basic.alldayprojectcommerce.domain.user.dto.response.GetmeUserResponse; import jpa.basic.alldayprojectcommerce.domain.user.service.UserCommandService; @@ -23,18 +24,27 @@ public class UserController { // 마이페이지 조회 @GetMapping("/me") - public ResponseEntity> getMyProfile(@LoginUser LoginUserInfo loginUser) { + public ResponseEntity> getMyProfile( + @LoginUser LoginUserInfo loginUser) { GetmeUserResponse response = userQueryService.getProfile(loginUser.id()); return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK, response)); } // 내 정보 수정 @PatchMapping("/me") - public ResponseEntity> updateMyProfile(@LoginUser LoginUserInfo loginUser, @RequestBody @Valid UpdatemeUserRequest request) { + public ResponseEntity> updateMyProfile( + @LoginUser LoginUserInfo loginUser, + @RequestBody @Valid UpdatemeUserRequest request) { userCommandService.updateProfile(loginUser.id(), request); return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK)); } - - + // 비밀번호 수정 + @PatchMapping("/password") + public ResponseEntity> updatePassword( + @LoginUser LoginUserInfo loginUser, + @RequestBody @Valid UpdatePasswordRequest request) { + userCommandService.updatePassword(loginUser.id(), request); + return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK)); + } } diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatePasswordRequest.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatePasswordRequest.java new file mode 100644 index 0000000..7a2f6dd --- /dev/null +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatePasswordRequest.java @@ -0,0 +1,14 @@ +package jpa.basic.alldayprojectcommerce.domain.user.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record UpdatePasswordRequest( + + @NotBlank(message = "현재 비밀번호를 입력하세요") + String currentPassword, + + @NotBlank + @Size(min = 8, max = 20, message = "새 비밀번호는 최소 8자 ~ 20자입니다.") + String newPassword +) {} diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatemeUserRequest.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatemeUserRequest.java index d4f8423..a1e794d 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatemeUserRequest.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/dto/request/UpdatemeUserRequest.java @@ -1,7 +1,5 @@ package jpa.basic.alldayprojectcommerce.domain.user.dto.request; -import jakarta.persistence.Column; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; @@ -10,16 +8,10 @@ public record UpdatemeUserRequest( @Size(max = 20, message = "이름은 최대 20자 입니다.") String name, - @Size(max = 100, message = "전화번호는 최대 100자 입니다.") @Pattern( regexp = "^01(?:0|1|[6-9])-(?:\\d{3}|\\d{4})-\\d{4}$", message = "휴대폰 번호 형식이 올바르지 않습니다. (예: 010-1234-5678)" ) String phone, - @Size(max = 255) - String address, - - //@NotBlank(message = "비밀번호는 필수 값입니다.") - @Size(min = 8, max = 20, message = "새 비밀번호는 최소 8자 ~ 20자입니다.") - String password) {} + String address) {} diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/entity/User.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/entity/User.java index b888805..678b0d7 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/entity/User.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/entity/User.java @@ -29,12 +29,14 @@ public class User extends BaseEntity { @Column(length = 100) private String phone; + @Column(length = 250) private String address; @Enumerated(EnumType.STRING) @Column(nullable = false) private UserRole role = UserRole.USER; + // 회원가입 public static User createUser(String email, String encodedPassword) { User user = new User(); user.email = email; @@ -42,10 +44,15 @@ public static User createUser(String email, String encodedPassword) { return user; } - public void updateProfile(String name, String encodedPassword, String phone, String address) { + // 내 정보 수정 + public void updateProfile(String name, String phone, String address) { if(name != null) this.name = name; - if(encodedPassword != null) this.password = encodedPassword; if(phone != null)this.phone = phone; if(address != null)this.address = address; } + + // 비밀번호 수정 + public void updatePassword(String encodedPassword) { + this.password = encodedPassword; + } } diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/repository/UserRepository.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/repository/UserRepository.java index 5213146..22e5ea9 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/repository/UserRepository.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/repository/UserRepository.java @@ -8,9 +8,12 @@ public interface UserRepository extends JpaRepository { + // 회원가입 시 이메일 중복확인 boolean existsUserByEmail(String email); + // 로그인 시 유저DB에 있는 email인지 조회 Optional findByEmail(String email); + //????? List id(Long id); } diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandService.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandService.java index a5ccd33..451802f 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandService.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandService.java @@ -1,5 +1,6 @@ package jpa.basic.alldayprojectcommerce.domain.user.service; +import jpa.basic.alldayprojectcommerce.domain.user.dto.request.UpdatePasswordRequest; import jpa.basic.alldayprojectcommerce.domain.user.dto.request.UpdatemeUserRequest; public interface UserCommandService { @@ -9,4 +10,7 @@ public interface UserCommandService { // 내 정보 수정 void updateProfile(Long userId, UpdatemeUserRequest request); + + // 비밀번호 수정 + void updatePassword(Long userId, UpdatePasswordRequest request); } diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandServiceImpl.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandServiceImpl.java index 949f99a..2da696f 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandServiceImpl.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/service/UserCommandServiceImpl.java @@ -2,6 +2,7 @@ import jpa.basic.alldayprojectcommerce.common.exception.CustomException; import jpa.basic.alldayprojectcommerce.common.exception.ErrorCode; +import jpa.basic.alldayprojectcommerce.domain.user.dto.request.UpdatePasswordRequest; import jpa.basic.alldayprojectcommerce.domain.user.dto.request.UpdatemeUserRequest; import jpa.basic.alldayprojectcommerce.domain.user.entity.User; import jpa.basic.alldayprojectcommerce.domain.user.repository.UserRepository; @@ -31,32 +32,31 @@ public void create(String email, String encodedPassword) { @Override public void updateProfile(Long userId, UpdatemeUserRequest request) { User user = userRepository.findById(userId) - .orElseThrow( () -> new CustomException(ErrorCode.USER_NOT_FOUND)); + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - // 이름, 전화번호, 주소 수정 - user.updateProfile(request.name(), request.password(), request.phone(), request.address()); - -// // 비밀번호 수정 (임시 비밀번호..) -// if(request.password() != null && !request.password().isBlank()) { -// user.updatePassword(passwordEncoder.encode(request.password())); -// } - // @Transactional에 의해 dirty-checking으로 자동 저장됨. + // 이름, 전화번호, 주소 수정내역 반영 + user.updateProfile(request.name(), request.phone(), request.address()); } - // 내 정보 수정 + // 비밀번호 변경 @Override - public void updateProfile(Long userId, UpdatemeUserRequest request) { + public void updatePassword(Long userId, UpdatePasswordRequest request){ User user = userRepository.findById(userId) - .orElseThrow( () -> new CustomException(ErrorCode.USER_NOT_FOUND)); + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - // 이름, 전화번호, 주소 수정 - user.updateProfile(request.name(), request.password(), request.phone(), request.address()); + // 1. 현재 비밀번호 검증 + if (!passwordEncoder.matches(request.currentPassword(), user.getPassword())) { + throw new CustomException(ErrorCode.USER_PASSWORD_NOT_MATCH); + } + + // 2. 현재 비밀번호랑 동일한지 검증 + if (passwordEncoder.matches(request.newPassword(), user.getPassword())) { + throw new CustomException(ErrorCode.USER_PASSWORD_SAME_AS_CURRENT); + } -// // 비밀번호 수정 (임시 비밀번호..) -// if(request.password() != null && !request.password().isBlank()) { -// user.updatePassword(passwordEncoder.encode(request.password())); -// } - // @Transactional에 의해 dirty-checking으로 자동 저장됨. + // 3. 새 비밀번호 인코딩 후 저장 + String encodedNewPassword = passwordEncoder.encode(request.newPassword()); + user.updatePassword(encodedNewPassword); } } From 51a2134647438c488a5521ab45915aeae043338a Mon Sep 17 00:00:00 2001 From: Hye_rin Mun Date: Fri, 17 Apr 2026 01:47:33 +0900 Subject: [PATCH 4/5] =?UTF-8?q?chore:=20=EC=82=AC=EC=86=8C=ED=95=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/repository/UserRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/repository/UserRepository.java b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/repository/UserRepository.java index 22e5ea9..6700c74 100644 --- a/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/repository/UserRepository.java +++ b/src/main/java/jpa/basic/alldayprojectcommerce/domain/user/repository/UserRepository.java @@ -14,6 +14,6 @@ public interface UserRepository extends JpaRepository { // 로그인 시 유저DB에 있는 email인지 조회 Optional findByEmail(String email); - //????? + // TODO: 어디에 사용하려 했는지 찾아보기 List id(Long id); } From ccd4b6f3137b593020c28f5aa1e60dc39a68841e Mon Sep 17 00:00:00 2001 From: Hye_rin Mun Date: Tue, 28 Apr 2026 00:28:13 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore:=20=EB=8B=A8=EC=88=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 7 ------ build.gradle | 3 --- src/main/resources/application.yml | 29 ---------------------- src/main/resources/templates/checkout.html | 4 +-- 4 files changed, 2 insertions(+), 41 deletions(-) diff --git a/.env.example b/.env.example index 01068e1..a4fde60 100644 --- a/.env.example +++ b/.env.example @@ -5,10 +5,3 @@ DB_USERNAME= DB_PASSWORD= JWT_SECRET_KEY= - -# ============================= -# PortOne -# ============================= -PORTONE_API_SECRET=V2_API_Secret -PORTONE_STORE_ID=store-ID -PORTONE_CHANNEL_KEY=channel-key- \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3491256..68c9df3 100644 --- a/build.gradle +++ b/build.gradle @@ -48,9 +48,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test' - // OAuth2 - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - implementation 'io.github.openfeign.querydsl:querydsl-jpa:6.10.1' annotationProcessor 'io.github.openfeign.querydsl:querydsl-apt:6.10.1:jakarta' annotationProcessor 'jakarta.annotation:jakarta.annotation-api' diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 31e7935..bea2dbd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,45 +6,16 @@ spring: active: ${SPRING_PROFILES_ACTIVE:local} # .env 또는 환경변수로 override 가능, 기본값 local jpa: open-in-view: false - security: - oauth2: - client: - registration: - google: - client-id: ${GOOGLE_CLIENT_ID} - client-secret: ${GOOGLE_CLIENT_SECRET} - scope: email, profile - kakao: - client-id: ${KAKAO_CLIENT_ID} - client-secret: ${KAKAO_CLIENT_SECRET} - client-authentication-method: client_secret_post - authorization-grant-type: authorization_code - redirect-uri: "{baseUrl}/login/oauth2/code/kakao" - scope: profile_nickname, account_email - provider: - kakao: - authorization-uri: https://kauth.kakao.com/oauth/authorize - token-uri: https://kauth.kakao.com/oauth/token - user-info-uri: https://kapi.kakao.com/v2/user/me - user-name-attribute: id data: redis: host: ${REDIS_HOST:localhost} port: ${REDIS_PORT:6379} -portone: - api-secret: ${PORTONE_API_SECRET} - store-id: ${PORTONE_STORE_ID} - channel-key: ${PORTONE_CHANNEL_KEY} - jwt: secret-key: ${JWT_SECRET_KEY} access-token-validity-time: 1800000 # 30분 (ms) refresh-token-validity-time: 604800000 # 7일 (ms) -oauth2: - redirect-uri: http://localhost:3000/oauth2/callback - management: endpoints: web: diff --git a/src/main/resources/templates/checkout.html b/src/main/resources/templates/checkout.html index 3d1021e..0f570ca 100644 --- a/src/main/resources/templates/checkout.html +++ b/src/main/resources/templates/checkout.html @@ -47,13 +47,13 @@

주문 상품

-

박경화

+

르탄이

주문자를 등록해주세요.