From 442fb475396dc56aa843f746a5bdbb4c4a28ae39 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Mon, 12 Jan 2026 20:37:40 +0900 Subject: [PATCH 01/83] =?UTF-8?q?merge=20=EC=84=B1=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/eatsfine/eatsfine/domain/booking/entity/Booking.java | 3 +-- .../java/com/eatsfine/eatsfine/domain/store/entity/Store.java | 3 +-- .../eatsfine/eatsfine/domain/storetable/entity/StoreTable.java | 3 +-- .../eatsfine/domain/table_layout/entity/TableLayout.java | 2 +- .../eatsfine/eatsfine/domain/tableimage/entity/TableImage.java | 2 +- .../eatsfine/global/{entity => common}/BaseEntity.java | 2 +- 6 files changed, 6 insertions(+), 9 deletions(-) rename src/main/java/com/eatsfine/eatsfine/global/{entity => common}/BaseEntity.java (93%) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/booking/entity/Booking.java b/src/main/java/com/eatsfine/eatsfine/domain/booking/entity/Booking.java index 5ec34cb1..45f3d741 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/booking/entity/Booking.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/booking/entity/Booking.java @@ -3,9 +3,8 @@ import com.eatsfine.eatsfine.domain.booking.entity.mapping.BookingTable; import com.eatsfine.eatsfine.domain.booking.enums.BookingStatus; import com.eatsfine.eatsfine.domain.store.entity.Store; -import com.eatsfine.eatsfine.domain.storetable.entity.StoreTable; import com.eatsfine.eatsfine.domain.user.entity.User; -import com.eatsfine.eatsfine.global.entity.BaseEntity; +import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java index c0e73d24..347e5c21 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java @@ -4,13 +4,12 @@ import com.eatsfine.eatsfine.domain.region.entity.Region; import com.eatsfine.eatsfine.domain.store.enums.Category; import com.eatsfine.eatsfine.domain.store.enums.StoreApprovalStatus; -import com.eatsfine.eatsfine.domain.storetable.entity.StoreTable; import com.eatsfine.eatsfine.domain.table_layout.entity.TableLayout; import com.eatsfine.eatsfine.domain.tableimage.entity.TableImage; import com.eatsfine.eatsfine.domain.user.entity.User; import com.eatsfine.eatsfine.global.apiPayload.code.status.ErrorStatus; import com.eatsfine.eatsfine.global.apiPayload.exception.GeneralException; -import com.eatsfine.eatsfine.global.entity.BaseEntity; +import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/storetable/entity/StoreTable.java b/src/main/java/com/eatsfine/eatsfine/domain/storetable/entity/StoreTable.java index 8bf83dc1..2fbb4fd8 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/storetable/entity/StoreTable.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/storetable/entity/StoreTable.java @@ -1,9 +1,8 @@ package com.eatsfine.eatsfine.domain.storetable.entity; -import com.eatsfine.eatsfine.domain.store.entity.Store; import com.eatsfine.eatsfine.domain.storetable.enums.SeatsType; import com.eatsfine.eatsfine.domain.table_layout.entity.TableLayout; -import com.eatsfine.eatsfine.global.entity.BaseEntity; +import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/entity/TableLayout.java b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/entity/TableLayout.java index 3b4956cc..9781fca3 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/entity/TableLayout.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/entity/TableLayout.java @@ -2,7 +2,7 @@ import com.eatsfine.eatsfine.domain.store.entity.Store; import com.eatsfine.eatsfine.domain.storetable.entity.StoreTable; -import com.eatsfine.eatsfine.global.entity.BaseEntity; +import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/tableimage/entity/TableImage.java b/src/main/java/com/eatsfine/eatsfine/domain/tableimage/entity/TableImage.java index 1bb000ef..f4a9e777 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/tableimage/entity/TableImage.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/tableimage/entity/TableImage.java @@ -1,7 +1,7 @@ package com.eatsfine.eatsfine.domain.tableimage.entity; import com.eatsfine.eatsfine.domain.store.entity.Store; -import com.eatsfine.eatsfine.global.entity.BaseEntity; +import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/com/eatsfine/eatsfine/global/entity/BaseEntity.java b/src/main/java/com/eatsfine/eatsfine/global/common/BaseEntity.java similarity index 93% rename from src/main/java/com/eatsfine/eatsfine/global/entity/BaseEntity.java rename to src/main/java/com/eatsfine/eatsfine/global/common/BaseEntity.java index a9d9405c..5ecf6ad1 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/entity/BaseEntity.java +++ b/src/main/java/com/eatsfine/eatsfine/global/common/BaseEntity.java @@ -1,4 +1,4 @@ -package com.eatsfine.eatsfine.global.entity; +package com.eatsfine.eatsfine.global.common; import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; From 8220906306180fc4b67194b442f0463926fc3ea6 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Mon, 12 Jan 2026 20:38:07 +0900 Subject: [PATCH 02/83] =?UTF-8?q?merge=20=EC=84=B1=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/businesshours/entity/BusinessHours.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/entity/BusinessHours.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/entity/BusinessHours.java index 6db43f24..c2a851e7 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/entity/BusinessHours.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/entity/BusinessHours.java @@ -1,7 +1,7 @@ package com.eatsfine.eatsfine.domain.businesshours.entity; import com.eatsfine.eatsfine.domain.store.entity.Store; -import com.eatsfine.eatsfine.global.entity.BaseEntity; +import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; From 5d487f82527d9f44284a5dd85c34c2023132c8b1 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Mon, 12 Jan 2026 20:48:37 +0900 Subject: [PATCH 03/83] =?UTF-8?q?[feature]:=20user=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 8 +++ .../user/controller/UserController.java | 5 ++ .../eatsfine/domain/user/dto/UserRequest.java | 4 -- .../domain/user/dto/UserRequestDto.java | 57 +++++++++++++++++++ .../domain/user/dto/UserResponseDto.java | 51 +++++++++++++++++ .../eatsfine/domain/user/entity/User.java | 49 +++++++++++++++- .../eatsfine/domain/user/enums/Grade.java | 4 ++ .../eatsfine/domain/user/enums/Role.java | 5 ++ .../domain/user/enums/SocialType.java | 7 +++ .../user/repository/UserRepository.java | 11 +++- .../domain/user/service/UserServiceImpl.java | 4 ++ .../validator/annotation/PasswordMatch.java | 20 +++++++ .../valid/PasswordMatchValidator.java | 4 ++ src/main/resources/application-local.yml | 8 +-- 14 files changed, 227 insertions(+), 10 deletions(-) delete mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserRequest.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserRequestDto.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserResponseDto.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/enums/Grade.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/enums/Role.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/enums/SocialType.java create mode 100644 src/main/java/com/eatsfine/eatsfine/global/validator/annotation/PasswordMatch.java create mode 100644 src/main/java/com/eatsfine/eatsfine/global/validator/valid/PasswordMatchValidator.java diff --git a/build.gradle b/build.gradle index 4e82f3fc..3a2ac997 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,14 @@ dependencies { //security //implementation 'org.springframework.boot:spring-boot-starter-security' + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // oauth + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + //swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.1' } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java index 0c538442..b8b749b5 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java @@ -1,4 +1,9 @@ package com.eatsfine.eatsfine.domain.user.controller; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor public class UserController { } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserRequest.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserRequest.java deleted file mode 100644 index 9528c2e4..00000000 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.eatsfine.eatsfine.domain.user.dto; - -public class UserRequest { -} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserRequestDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserRequestDto.java new file mode 100644 index 00000000..d3e2476c --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserRequestDto.java @@ -0,0 +1,57 @@ +package com.eatsfine.eatsfine.domain.user.dto; + +import com.eatsfine.eatsfine.global.validator.annotation.PasswordMatch; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import lombok.Setter; + +public class UserRequestDto { + + @PasswordMatch + @Getter + public static class JoinDto{ + + @NotBlank(message = "이름은 필수입니다.") + private String name; // 이름 + + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "유효한 이메일 형식이어야 합니다.") + private String email; // 이메일 + + @NotBlank(message = "휴대전화 번호는 필수입니다.") + @Pattern(regexp = "^010\\d{8}$", message = "휴대전화 번호는 010으로 시작하는 11자리 숫자여야 합니다.") + private String phoneNumber; // 휴대전화 번호 + + @NotBlank(message = "비밀번호는 필수 입니다.") + @Pattern( + regexp = "^(?=(.*[a-zA-Z].*[0-9])|(?=.*[a-zA-Z].*[!@#$%^&*])|(?=.*[0-9].*[!@#$%^&*]))[a-zA-Z0-9!@#$%^&*]{8,20}$", + message = "비밀번호는 영문, 숫자, 특수문자 중 2가지 이상 조합이며, 8자 ~20자 이내 이어야 합니다." + ) + private String password; + + @NotBlank(message = "비밀번호 확인은 필수입니다.") + private String passwordConfirm; // 비밀번호 확인 + + } + + @Getter + public static class LoginDto { + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "유효한 이메일 형식이어야 합니다.") + private String email; + + @NotBlank(message = "비밀번호는 필수입니다.") + private String password; + } + + @Getter + @Setter + public static class UpdateDto { + private String profileImage; + private String email; + private String nickName; + private String phoneNumber; + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserResponseDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserResponseDto.java new file mode 100644 index 00000000..eab7775b --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserResponseDto.java @@ -0,0 +1,51 @@ +package com.eatsfine.eatsfine.domain.user.dto; + +import lombok.*; + +import java.time.LocalDateTime; + +public class UserResponseDto { + + @Builder + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class JoinResultDto{ + private Long id; + private LocalDateTime createdAt; + } + + @Builder + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class LoginResponseDto{ + private Long id; + private String accessToken; + private String refreshToken; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class UserInfoDto{ + private Long id; + private String profileImage; + private String email; + private String nickName; + private String phoneNumber; + } + + @Getter + @Setter + @Builder + public static class UpdateResponseDto{ + private String profileImage; + private String email; + private String nickName; + private String phoneNumber; + } + + +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java index d6279e57..dd319bb6 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java @@ -1,5 +1,8 @@ package com.eatsfine.eatsfine.domain.user.entity; +import com.eatsfine.eatsfine.domain.user.enums.Role; +import com.eatsfine.eatsfine.domain.user.enums.SocialType; +import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; @@ -10,9 +13,53 @@ @AllArgsConstructor @Builder @Table(name = "users") -public class User { +public class User extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Column(nullable = false, length = 20) + private String name; + + @Column(nullable = false, length = 20) + private String nickName; + + @Column(nullable = false, unique = true) + private String email; + + private String password; + + @Column(nullable = false, length = 20) + private String phoneNumber; + + @Enumerated(EnumType.STRING) + private Role role; + + @Column(name = "social_id", unique = true) + private String socialId; + + @Enumerated(EnumType.STRING) + @Column(name = "social_type") + private SocialType socialType; + + @Setter + @Column(nullable = true) + private String profileImage; + + public void updateNickname(String nickName){ + this.nickName = nickName; + } + + public void updatePhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public void updateEmail(String email) { + this.email = email; + } + + public void updateProfileImage(String profileImage) { + this.profileImage = profileImage; + } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/enums/Grade.java b/src/main/java/com/eatsfine/eatsfine/domain/user/enums/Grade.java new file mode 100644 index 00000000..6f0b61c3 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/enums/Grade.java @@ -0,0 +1,4 @@ +package com.eatsfine.eatsfine.domain.user.enums; + +public enum Grade { +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/enums/Role.java b/src/main/java/com/eatsfine/eatsfine/domain/user/enums/Role.java new file mode 100644 index 00000000..857808c1 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/enums/Role.java @@ -0,0 +1,5 @@ +package com.eatsfine.eatsfine.domain.user.enums; + +public enum Role { + ROLE_CUSTOMER, ROLE_OWNER +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/enums/SocialType.java b/src/main/java/com/eatsfine/eatsfine/domain/user/enums/SocialType.java new file mode 100644 index 00000000..a0ef11b6 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/enums/SocialType.java @@ -0,0 +1,7 @@ +package com.eatsfine.eatsfine.domain.user.enums; + +public enum SocialType { + NAVER, + KAKAO, + GOOGLE +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/repository/UserRepository.java b/src/main/java/com/eatsfine/eatsfine/domain/user/repository/UserRepository.java index 6caff489..734fa4ce 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/repository/UserRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/repository/UserRepository.java @@ -1,4 +1,13 @@ package com.eatsfine.eatsfine.domain.user.repository; -public interface UserRepository { +import com.eatsfine.eatsfine.domain.user.entity.User; +import com.eatsfine.eatsfine.domain.user.enums.SocialType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + + Optional findBySocialTypeAndSocialId(SocialType socialType, String socialId); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java index d4457ffb..8f2bbdd1 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java @@ -1,4 +1,8 @@ package com.eatsfine.eatsfine.domain.user.service; + +import org.springframework.stereotype.Service; + +@Service public class UserServiceImpl { } diff --git a/src/main/java/com/eatsfine/eatsfine/global/validator/annotation/PasswordMatch.java b/src/main/java/com/eatsfine/eatsfine/global/validator/annotation/PasswordMatch.java new file mode 100644 index 00000000..14190ba7 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/global/validator/annotation/PasswordMatch.java @@ -0,0 +1,20 @@ +package com.eatsfine.eatsfine.global.validator.annotation; + +import com.eatsfine.eatsfine.global.validator.valid.PasswordMatchValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = PasswordMatchValidator.class) +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PasswordMatch { + String message() default "비밀번호와 비밀번호 확인이 일치하지 않습니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/src/main/java/com/eatsfine/eatsfine/global/validator/valid/PasswordMatchValidator.java b/src/main/java/com/eatsfine/eatsfine/global/validator/valid/PasswordMatchValidator.java new file mode 100644 index 00000000..18d68574 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/global/validator/valid/PasswordMatchValidator.java @@ -0,0 +1,4 @@ +package com.eatsfine.eatsfine.global.validator.valid; + +public class PasswordMatchValidator { +} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 56548126..cdcfc108 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,16 +1,16 @@ -server: - port: 8080 - profile: local +port: 8080 +profile: local spring: config: activate: on-profile: local + datasource: url: jdbc:mysql://localhost:3306/eatsfine_local?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&serverTimezone=Asia/Seoul username: root - password: 0766wjd! + password: password driver-class-name: com.mysql.cj.jdbc.Driver data: redis: From af53b10e6dd0d0eee73ae6718507f6d2cb82e218 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Mon, 19 Jan 2026 16:09:03 +0900 Subject: [PATCH 04/83] =?UTF-8?q?[Feature]=20JWT=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/exception/UserException.java | 10 ++ .../domain/user/status/UserErrorStatus.java | 46 +++++++ .../config/jwt/JwtAuthenticationFilter.java | 66 +++++++++++ .../global/config/jwt/JwtTokenProvider.java | 112 ++++++++++++++++++ .../global/config/properties/Constants.java | 6 + .../config/properties/JwtProperties.java | 15 +++ 6 files changed, 255 insertions(+) create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/exception/UserException.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java create mode 100644 src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtTokenProvider.java create mode 100644 src/main/java/com/eatsfine/eatsfine/global/config/properties/Constants.java create mode 100644 src/main/java/com/eatsfine/eatsfine/global/config/properties/JwtProperties.java diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/UserException.java b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/UserException.java new file mode 100644 index 00000000..114cce1f --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/UserException.java @@ -0,0 +1,10 @@ +package com.eatsfine.eatsfine.domain.user.exception; + +import com.eatsfine.eatsfine.global.apiPayload.code.BaseErrorCode; +import com.eatsfine.eatsfine.global.apiPayload.exception.GeneralException; + +public class UserException extends GeneralException { + public UserException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java new file mode 100644 index 00000000..e7148b98 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java @@ -0,0 +1,46 @@ +package com.eatsfine.eatsfine.domain.user.status; + +import com.eatsfine.eatsfine.global.apiPayload.code.BaseErrorCode; +import com.eatsfine.eatsfine.global.apiPayload.code.ErrorReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum UserErrorStatus implements BaseErrorCode { + + // 멤버 관련 에러 + MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."), + NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다."), + + // 토큰 유효 에러 + INVALID_TOKEN(HttpStatus.NOT_FOUND, "TOKEN4001", "토큰이 없습니다."), + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ErrorReasonDto getReason() { + return ErrorReasonDto.builder() + .isSuccess(false) + .message(message) + .code(code) + .build(); + } + + @Override + public ErrorReasonDto getReasonHttpStatus() { + return ErrorReasonDto.builder() + .httpStatus(httpStatus) + .isSuccess(false) + .code(code) + .message(message) + .build(); + } + + + + + } diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java new file mode 100644 index 00000000..9f6ce7bf --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,66 @@ +package com.eatsfine.eatsfine.global.config.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + private final UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain chain) + throws ServletException, IOException { + + String uri = request.getRequestURI(); + System.out.println("요청 URI: " + uri); + + // 인증 없이 통과시킬 경로들 + if (uri.startsWith("/api/v1/auth/login") || + uri.startsWith("/api/v1/auth/register") || + uri.startsWith("/api/v1/auth/reissue") || + uri.startsWith("/user/reissue") || + uri.startsWith("/oauth2") || + uri.startsWith("/login")) { + chain.doFilter(request, response); + return; + } + + + String token = JwtTokenProvider.resolveToken(request); + + if (token != null && jwtTokenProvider.validateToken(token)) { + try { + String email = jwtTokenProvider.getEmailFromToken(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(email); + + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (Exception e) { + // 예외 로그 출력 + System.out.println("JWT 인증 오류: " + e.getMessage()); + } + } + + chain.doFilter(request, response); + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtTokenProvider.java b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtTokenProvider.java new file mode 100644 index 00000000..67028e09 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtTokenProvider.java @@ -0,0 +1,112 @@ +package com.eatsfine.eatsfine.global.config.jwt; + +import com.eatsfine.eatsfine.domain.user.exception.UserException; +import com.eatsfine.eatsfine.global.config.properties.Constants; +import org.springframework.security.core.userdetails.User; +import com.eatsfine.eatsfine.global.apiPayload.code.status.ErrorStatus; +import com.eatsfine.eatsfine.global.config.properties.JwtProperties; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Collections; +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + + private final JwtProperties jwtProperties; + + private Key getSigningKey() { + return Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)); + } + + private final long accessTokenValidity = 1000L * 60 * 60; // 1시간 + private final long refreshTokenValidity = 1000L * 60 * 60 * 24 * 7; // 7일 + + public String createAccessToken(String email) { + Date now = new Date(); + Date expiry = new Date(now.getTime() + accessTokenValidity); + return Jwts.builder() + .setSubject(email) + .setIssuedAt(now) + .setExpiration(expiry) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public String createRefreshToken(String email) { + Date now = new Date(); + Date expiry = new Date(now.getTime() + refreshTokenValidity); + return Jwts.builder() + .setSubject(email) + .setIssuedAt(now) + .setExpiration(expiry) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token); // 유효성 검증 + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } + + public Authentication getAuthentication(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + + String email = claims.getSubject(); + + User principal = new User(email, "", Collections.emptyList()); + return new UsernamePasswordAuthenticationToken(principal, token, principal.getAuthorities()); + } + + public static String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(Constants.AUTH_HEADER); + + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(Constants.TOKEN_PREFIX)) { + return bearerToken.substring(Constants.TOKEN_PREFIX.length()); + } + return null; + } + + public Authentication extractAuthentication(HttpServletRequest request) { + String accessToken = resolveToken(request); + if (accessToken == null || !validateToken(accessToken)) { + throw new UserException(ErrorStatus.INVALID_TOKEN); + } + return getAuthentication(accessToken); + } + + public String getEmailFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody() + .getSubject(); + + } + +} diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/properties/Constants.java b/src/main/java/com/eatsfine/eatsfine/global/config/properties/Constants.java new file mode 100644 index 00000000..d3e0d94f --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/global/config/properties/Constants.java @@ -0,0 +1,6 @@ +package com.eatsfine.eatsfine.global.config.properties; + +public final class Constants { + public static final String AUTH_HEADER = "Authorization"; + public static final String TOKEN_PREFIX = "Bearer "; +} \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/properties/JwtProperties.java b/src/main/java/com/eatsfine/eatsfine/global/config/properties/JwtProperties.java new file mode 100644 index 00000000..c81b91ab --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/global/config/properties/JwtProperties.java @@ -0,0 +1,15 @@ +package com.eatsfine.eatsfine.global.config.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@Getter +@Setter +@ConfigurationProperties("jwt") +public class JwtProperties { + private String secret; + +} \ No newline at end of file From f84b7697203e7e5e323c5bc54ecfa054efc8d537 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Mon, 19 Jan 2026 16:09:28 +0900 Subject: [PATCH 05/83] =?UTF-8?q?[FIX]=20=EC=98=A4=EB=A5=98=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 --- .../eatsfine/global/apiPayload/code/status/ErrorStatus.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/global/apiPayload/code/status/ErrorStatus.java index 761d33b3..5ca57612 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/global/apiPayload/code/status/ErrorStatus.java @@ -32,7 +32,7 @@ public enum ErrorStatus implements BaseErrorCode { @Override public ErrorReasonDto getReason() { return ErrorReasonDto.builder() - .isSuccess(true) + .isSuccess(false) .message(message) .code(code) .build(); @@ -42,7 +42,7 @@ public ErrorReasonDto getReason() { public ErrorReasonDto getReasonHttpStatus() { return ErrorReasonDto.builder() .httpStatus(httpStatus) - .isSuccess(true) + .isSuccess(false) .code(code) .message(message) .build(); From 37be66f2a6a6b2f7c8d47e3ba5a5fdfeae91c5bd Mon Sep 17 00:00:00 2001 From: SungMinju Date: Mon, 19 Jan 2026 16:10:04 +0900 Subject: [PATCH 06/83] =?UTF-8?q?[FIX]=20User=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/user/dto/UserRequestDto.java | 2 +- .../com/eatsfine/eatsfine/domain/user/entity/User.java | 3 --- .../domain/user/repository/UserRepository.java | 10 +++++++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserRequestDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserRequestDto.java index d3e2476c..703b1f0b 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserRequestDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserRequestDto.java @@ -14,7 +14,7 @@ public class UserRequestDto { public static class JoinDto{ @NotBlank(message = "이름은 필수입니다.") - private String name; // 이름 + private String nickName; // 이름 @NotBlank(message = "이메일은 필수입니다.") @Email(message = "유효한 이메일 형식이어야 합니다.") diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java index dd319bb6..375f31fe 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java @@ -19,9 +19,6 @@ public class User extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false, length = 20) - private String name; - @Column(nullable = false, length = 20) private String nickName; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/repository/UserRepository.java b/src/main/java/com/eatsfine/eatsfine/domain/user/repository/UserRepository.java index 6caff489..b1614ca0 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/repository/UserRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/repository/UserRepository.java @@ -1,4 +1,12 @@ package com.eatsfine.eatsfine.domain.user.repository; -public interface UserRepository { +import com.eatsfine.eatsfine.domain.user.entity.User; +import com.eatsfine.eatsfine.domain.user.enums.SocialType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + Optional findBySocialTypeAndSocialId(SocialType socialType, String socialId); } From 830754554a8cfa1d941ca0b41c810d7b41aa8a93 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Mon, 19 Jan 2026 16:10:27 +0900 Subject: [PATCH 07/83] =?UTF-8?q?[Feature]=20User=20=EC=BB=A8=EB=B2=84?= =?UTF-8?q?=ED=84=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/converter/UserConverter.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java index ddce98db..44120576 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java @@ -1,4 +1,18 @@ package com.eatsfine.eatsfine.domain.user.converter; +import com.eatsfine.eatsfine.domain.user.dto.UserResponseDto; +import com.eatsfine.eatsfine.domain.user.entity.User; + public class UserConverter { + + public static UserResponseDto.UserInfoDto toUserInfo(User user) { + return UserResponseDto.UserInfoDto.builder() + .id(user.getId()) + .email(user.getEmail()) + .nickName(user.getNickName()) + .phoneNumber(user.getPhoneNumber()) + .profileImage(user.getProfileImage()) + .build(); + } + } From c89395e7390a51cf06e00118bb70f30521106022 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Mon, 19 Jan 2026 16:16:59 +0900 Subject: [PATCH 08/83] =?UTF-8?q?[FIX]=20=EC=98=A4=EB=A5=98=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 --- .../eatsfine/eatsfine/domain/user/status/UserErrorStatus.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java index e7148b98..c27eba75 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java @@ -15,7 +15,7 @@ public enum UserErrorStatus implements BaseErrorCode { NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다."), // 토큰 유효 에러 - INVALID_TOKEN(HttpStatus.NOT_FOUND, "TOKEN4001", "토큰이 없습니다."), + INVALID_TOKEN(HttpStatus.NOT_FOUND, "TOKEN4001", "토큰이 없습니다."); private final HttpStatus httpStatus; private final String code; From e27123039982bb83c343cce760aeeb4285ffe916 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Mon, 19 Jan 2026 16:18:40 +0900 Subject: [PATCH 09/83] =?UTF-8?q?[FIX]=20=EC=84=A4=EC=A0=95=EA=B0=92=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-local.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 56548126..72e2e1a8 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -8,18 +8,22 @@ spring: activate: on-profile: local datasource: - url: jdbc:mysql://localhost:3306/eatsfine_local?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&serverTimezone=Asia/Seoul - username: root - password: 0766wjd! + url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&serverTimezone=Asia/Seoul + username: ${DB_USERNAME} + password: ${DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver data: redis: - host: localhost - port: 6379 + host: ${REDIS_HOST} + port: ${REDIS_PORT} jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: - format_sql: true \ No newline at end of file + format_sql: true + +payment: + toss: + widget-secret-key: test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6 \ No newline at end of file From 16e29eeedcb0451cea16cdf4d4ee31510761f215 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Mon, 19 Jan 2026 22:41:42 +0900 Subject: [PATCH 10/83] =?UTF-8?q?[Refactor]=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/dto/{ => request}/UserRequestDto.java | 2 +- .../domain/user/dto/{ => response}/UserResponseDto.java | 2 +- .../global/apiPayload/code/status/ErrorStatus.java | 7 ------- 3 files changed, 2 insertions(+), 9 deletions(-) rename src/main/java/com/eatsfine/eatsfine/domain/user/dto/{ => request}/UserRequestDto.java (97%) rename src/main/java/com/eatsfine/eatsfine/domain/user/dto/{ => response}/UserResponseDto.java (94%) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserRequestDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java similarity index 97% rename from src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserRequestDto.java rename to src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java index 703b1f0b..91d903c2 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserRequestDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java @@ -1,4 +1,4 @@ -package com.eatsfine.eatsfine.domain.user.dto; +package com.eatsfine.eatsfine.domain.user.dto.request; import com.eatsfine.eatsfine.global.validator.annotation.PasswordMatch; import jakarta.validation.constraints.Email; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserResponseDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java similarity index 94% rename from src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserResponseDto.java rename to src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java index eab7775b..0b7f8d77 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/UserResponseDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java @@ -1,4 +1,4 @@ -package com.eatsfine.eatsfine.domain.user.dto; +package com.eatsfine.eatsfine.domain.user.dto.response; import lombok.*; diff --git a/src/main/java/com/eatsfine/eatsfine/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/global/apiPayload/code/status/ErrorStatus.java index 5ca57612..40ea069e 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/global/apiPayload/code/status/ErrorStatus.java @@ -15,13 +15,6 @@ public enum ErrorStatus implements BaseErrorCode { _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), _NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON404", "존재하지 않는 요청입니다."), - // 멤버 관련 에러 - MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."), - NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다."), - - // 토큰 유효 에러 - INVALID_TOKEN(HttpStatus.NOT_FOUND, "TOKEN4001", "토큰이 없습니다."), - // 예약금 관련 에러 PAYMENT_INVALID_DEPOSIT(HttpStatus.BAD_REQUEST, "PAYMENT4001", "예약금이 유효하지 않습니다."); From dcc3412b60c2ad77542f1cb0a9de1209296da0ef Mon Sep 17 00:00:00 2001 From: SungMinju Date: Mon, 19 Jan 2026 22:42:27 +0900 Subject: [PATCH 11/83] =?UTF-8?q?[Refactor]=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/eatsfine/domain/user/converter/UserConverter.java | 2 +- .../eatsfine/eatsfine/domain/user/service/UserServiceImpl.java | 2 ++ .../eatsfine/global/validator/valid/PasswordMatchValidator.java | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java index 44120576..a9f0822e 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java @@ -1,6 +1,6 @@ package com.eatsfine.eatsfine.domain.user.converter; -import com.eatsfine.eatsfine.domain.user.dto.UserResponseDto; +import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; import com.eatsfine.eatsfine.domain.user.entity.User; public class UserConverter { diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java index 8f2bbdd1..96fe2e5f 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java @@ -1,8 +1,10 @@ package com.eatsfine.eatsfine.domain.user.service; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class UserServiceImpl { } diff --git a/src/main/java/com/eatsfine/eatsfine/global/validator/valid/PasswordMatchValidator.java b/src/main/java/com/eatsfine/eatsfine/global/validator/valid/PasswordMatchValidator.java index 0ce53318..175073bd 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/validator/valid/PasswordMatchValidator.java +++ b/src/main/java/com/eatsfine/eatsfine/global/validator/valid/PasswordMatchValidator.java @@ -1,6 +1,6 @@ package com.eatsfine.eatsfine.global.validator.valid; -import com.eatsfine.eatsfine.domain.user.dto.UserRequestDto; +import com.eatsfine.eatsfine.domain.user.dto.request.UserRequestDto; import com.eatsfine.eatsfine.global.validator.annotation.PasswordMatch; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; From 107a30c38d0fdebafaa4d9d2996583784ade2ca3 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Mon, 19 Jan 2026 22:50:08 +0900 Subject: [PATCH 12/83] =?UTF-8?q?[Fix]=20=EC=98=A4=EB=A5=98=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=98=A4=EB=A5=98=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 --- .../eatsfine/eatsfine/global/config/jwt/JwtTokenProvider.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtTokenProvider.java b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtTokenProvider.java index 67028e09..f578c363 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtTokenProvider.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtTokenProvider.java @@ -1,6 +1,7 @@ package com.eatsfine.eatsfine.global.config.jwt; import com.eatsfine.eatsfine.domain.user.exception.UserException; +import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; import com.eatsfine.eatsfine.global.config.properties.Constants; import org.springframework.security.core.userdetails.User; import com.eatsfine.eatsfine.global.apiPayload.code.status.ErrorStatus; @@ -94,7 +95,7 @@ public static String resolveToken(HttpServletRequest request) { public Authentication extractAuthentication(HttpServletRequest request) { String accessToken = resolveToken(request); if (accessToken == null || !validateToken(accessToken)) { - throw new UserException(ErrorStatus.INVALID_TOKEN); + throw new UserException(UserErrorStatus.INVALID_TOKEN); } return getAuthentication(accessToken); } From 9943956e12c08a9c4cb295a4e350a87aeb2b5d07 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 30 Jan 2026 01:11:34 +0900 Subject: [PATCH 13/83] =?UTF-8?q?[Feat]=20=EC=95=BD=EA=B4=80=EB=8F=99?= =?UTF-8?q?=EC=9D=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/term/entity/Term.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java diff --git a/src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java b/src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java new file mode 100644 index 00000000..3bc78a0f --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java @@ -0,0 +1,38 @@ +package com.eatsfine.eatsfine.domain.term.entity; + +import com.eatsfine.eatsfine.domain.user.entity.User; +import com.eatsfine.eatsfine.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "term") +public class Term extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false, unique = true) + private User user; + + @Builder.Default + @Column(name = "tos_consent", nullable = false) + private Boolean tosConsent = true; + + @Builder.Default + @Column(name = "privacy_consent", nullable = false) + private Boolean privacyConsent = true; + + + @Column(name = "marketing_consent", nullable = false) + private Boolean marketingConsent; + +} From 32482b81a0f4d911877ad2b97a2a4e8fb026db62 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 30 Jan 2026 01:12:01 +0900 Subject: [PATCH 14/83] =?UTF-8?q?[Fix]=20=EC=95=BD=EA=B4=80=EB=8F=99?= =?UTF-8?q?=EC=9D=98=20dto=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/dto/request/UserRequestDto.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java index 91d903c2..08149627 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java @@ -1,8 +1,10 @@ package com.eatsfine.eatsfine.domain.user.dto.request; import com.eatsfine.eatsfine.global.validator.annotation.PasswordMatch; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import lombok.Getter; import lombok.Setter; @@ -34,6 +36,18 @@ public static class JoinDto{ @NotBlank(message = "비밀번호 확인은 필수입니다.") private String passwordConfirm; // 비밀번호 확인 + @NotNull(message = "이용약관에 동의합니다.") + @Schema(description = "서비스 이용약관 동의 여부 (필수)", example = "true") + private Boolean tosConsent; + + @NotNull(message = "개인정보 처리방침에 동의합니다") + @Schema(description = "개인정보 수집 및 이용 동의 여부 (필수)", example = "true") + private Boolean privacyConsent; + + @NotNull(message = "마케팅 정보 수신에 동의합니다") + @Schema(description = "마케팅 정보 수신 동의 여부 (선택)", example = "false") + private Boolean marketingConsent; + } @Getter From 8d19b22b130f52ab49e1cf66c20162101db670e0 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 30 Jan 2026 03:02:27 +0900 Subject: [PATCH 15/83] =?UTF-8?q?[Fix]=20=EC=95=BD=EA=B4=80=EB=8F=99?= =?UTF-8?q?=EC=9D=98=20dto=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/dto/request/UserRequestDto.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java index 08149627..64dc61fe 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java @@ -68,4 +68,25 @@ public static class UpdateDto { private String nickName; private String phoneNumber; } + + @Getter + @PasswordMatch + public static class ChangePasswordDto { + + @NotBlank(message = "현재 비밀번호는 필수입니다.") + @Schema(description = "현재 비밀번호", example = "CurrentPw!123") + private String currentPassword; + + @NotBlank(message = "새 비밀번호는 필수입니다.") + @Pattern( + regexp = "^(?=(.*[a-zA-Z].*[0-9])|(?=.*[a-zA-Z].*[!@#$%^&*])|(?=.*[0-9].*[!@#$%^&*]))[a-zA-Z0-9!@#$%^&*]{8,20}$", + message = "새 비밀번호는 영문, 숫자, 특수문자 중 2가지 이상 조합이며, 8자 ~20자 이내 이어야 합니다." + ) + @Schema(description = "새 비밀번호", example = "NewPw!1234") + private String newPassword; + + @NotBlank(message = "새 비밀번호 확인은 필수입니다.") + @Schema(description = "새 비밀번호 확인", example = "NewPw!1234") + private String newPasswordConfirm; + } } From c8490100dd92277ff5887b409d9860a7ce5c7a93 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 30 Jan 2026 03:03:02 +0900 Subject: [PATCH 16/83] =?UTF-8?q?[Fix]=20refresh=20token=20dto=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/dto/response/UserResponseDto.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java index 0b7f8d77..24e801fe 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java @@ -1,5 +1,6 @@ package com.eatsfine.eatsfine.domain.user.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; @@ -22,7 +23,6 @@ public static class JoinResultDto{ public static class LoginResponseDto{ private Long id; private String accessToken; - private String refreshToken; } @Builder @@ -47,5 +47,19 @@ public static class UpdateResponseDto{ private String phoneNumber; } + @Getter + @Builder + @AllArgsConstructor + public static class UpdatePasswordDto { + + @Schema(description = "비밀번호 변경 완료 여부", example = "true") + private boolean changed; + + @Schema(description = "비밀번호 변경 완료 시각", example = "2026-01-30T18:25:43") + private LocalDateTime changedAt; + + @Schema(description = "응답 메시지", example = "비밀번호가 성공적으로 변경되었습니다.") + private String message; + } } From 4acbc0973b53f6c3a36581e95020e604957e6aab Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 30 Jan 2026 03:03:33 +0900 Subject: [PATCH 17/83] =?UTF-8?q?[Fix]=20=EC=9C=A0=EC=A0=80=20=EC=BB=A8?= =?UTF-8?q?=EB=B2=84=ED=84=B0=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/converter/UserConverter.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java index a9f0822e..9227442b 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java @@ -1,17 +1,90 @@ package com.eatsfine.eatsfine.domain.user.converter; +import com.eatsfine.eatsfine.domain.user.dto.request.UserRequestDto; import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; import com.eatsfine.eatsfine.domain.user.entity.User; +import com.eatsfine.eatsfine.domain.user.enums.SocialType; + +import java.time.LocalDateTime; + +import static com.eatsfine.eatsfine.domain.user.enums.Role.ROLE_CUSTOMER; public class UserConverter { + public static UserResponseDto.JoinResultDto toJoinResult(User user) { + return UserResponseDto.JoinResultDto.builder() + .id(user.getId()) + .createdAt(user.getCreatedAt()) + .build(); + } + + + //로그인 응답 변환 + public static UserResponseDto.LoginResponseDto toLoginResponse(User user, String accessToken, String refreshToken) { + return UserResponseDto.LoginResponseDto.builder() + .id(user.getId()) + .accessToken(accessToken) + .build(); + } + + + // 유저 정보 조회 응답 변환 public static UserResponseDto.UserInfoDto toUserInfo(User user) { return UserResponseDto.UserInfoDto.builder() .id(user.getId()) + .profileImage(user.getProfileImage()) .email(user.getEmail()) .nickName(user.getNickName()) .phoneNumber(user.getPhoneNumber()) + .build(); + } + + + //유저 정보 수정 응답 변환 + public static UserResponseDto.UpdateResponseDto toUpdateResponse(User user) { + return UserResponseDto.UpdateResponseDto.builder() .profileImage(user.getProfileImage()) + .email(user.getEmail()) + .nickName(user.getNickName()) + .phoneNumber(user.getPhoneNumber()) + .build(); + } + + + // 비밀번호 변경 응답 변환 + public static UserResponseDto.UpdatePasswordDto toUpdatePasswordResponse(boolean changed, LocalDateTime changedAt, String message) { + return UserResponseDto.UpdatePasswordDto.builder() + .changed(changed) + .changedAt(changedAt) + .message(message) + .build(); + } + + + public static User toUser(UserRequestDto.JoinDto dto, String encodedPassword) { + return User.builder() + .nickName(dto.getNickName()) + .email(dto.getEmail()) + .phoneNumber(dto.getPhoneNumber()) + .password(encodedPassword) + .role(ROLE_CUSTOMER) // 기본 권한 + .build(); + } + + + /* + 소셜 유저 생성 (최초 소셜 가입 등) + -소셜 로그인에서 email/nickname/phoneNumber 등을 확보한 후 엔티티 생성에 사용 + */ + public static User toSocialUser(String email, String nickName, String phoneNumber, String socialId, SocialType socialType) { + + return User.builder() + .email(email) + .nickName(nickName) + .phoneNumber(phoneNumber) + .socialId(socialId) + .socialType(socialType) + .role(ROLE_CUSTOMER) .build(); } From af9b5a4df7a5b05d147849c94a9ea6b919a30193 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 30 Jan 2026 03:42:38 +0900 Subject: [PATCH 18/83] =?UTF-8?q?[Refactor]=20Refresh=20Token=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/eatsfine/domain/user/converter/UserConverter.java | 3 ++- .../eatsfine/domain/user/dto/response/UserResponseDto.java | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java index 9227442b..824fae7b 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java @@ -20,10 +20,11 @@ public static UserResponseDto.JoinResultDto toJoinResult(User user) { //로그인 응답 변환 - public static UserResponseDto.LoginResponseDto toLoginResponse(User user, String accessToken, String refreshToken) { + public static UserResponseDto.LoginResponseDto toLoginResponse(User user, String accessToken) { return UserResponseDto.LoginResponseDto.builder() .id(user.getId()) .accessToken(accessToken) + .refreshToken(null) .build(); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java index 24e801fe..a81b0338 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java @@ -23,6 +23,7 @@ public static class JoinResultDto{ public static class LoginResponseDto{ private Long id; private String accessToken; + private String refreshToken; } @Builder From 02390898c5669baf6b19f00f85e79324de7d30f5 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 30 Jan 2026 03:43:20 +0900 Subject: [PATCH 19/83] =?UTF-8?q?[Feat]=20Auth=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/CustomAccessDeniedHandler.java | 26 ++++++ .../auth/CustomAuthenticationEntryPoint.java | 18 ++++ .../global/auth/UserDetailsServiceImpl.java | 34 +++++++ .../global/config/SecurityConfig.java | 89 +++++++++++++++++++ .../config/jwt/JwtAuthenticationFilter.java | 6 +- 5 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/eatsfine/eatsfine/global/auth/CustomAccessDeniedHandler.java create mode 100644 src/main/java/com/eatsfine/eatsfine/global/auth/CustomAuthenticationEntryPoint.java create mode 100644 src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java create mode 100644 src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java diff --git a/src/main/java/com/eatsfine/eatsfine/global/auth/CustomAccessDeniedHandler.java b/src/main/java/com/eatsfine/eatsfine/global/auth/CustomAccessDeniedHandler.java new file mode 100644 index 00000000..8c4ea21b --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/global/auth/CustomAccessDeniedHandler.java @@ -0,0 +1,26 @@ +package com.eatsfine.eatsfine.global.auth; + + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; + +import java.io.IOException; + +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) + throws IOException, ServletException { + + response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403 + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"message\": \"접근 권한이 없습니다.\"}"); + } +} \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/global/auth/CustomAuthenticationEntryPoint.java b/src/main/java/com/eatsfine/eatsfine/global/auth/CustomAuthenticationEntryPoint.java new file mode 100644 index 00000000..b9ee691d --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/global/auth/CustomAuthenticationEntryPoint.java @@ -0,0 +1,18 @@ +package com.eatsfine.eatsfine.global.auth; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "인증이 필요합니다."); + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java new file mode 100644 index 00000000..190a1887 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java @@ -0,0 +1,34 @@ +package com.eatsfine.eatsfine.global.auth; + + + + +import com.eatsfine.eatsfine.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UserDetailsServiceImpl implements UserDetailsService { + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + com.eatsfine.eatsfine.domain.user.entity.User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다: " + email)); + + String password = user.getPassword(); + if (password == null) { + password = ""; + } + + return new User(user.getEmail(), password, List.of()); + } +} + diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java new file mode 100644 index 00000000..8b1a15fd --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java @@ -0,0 +1,89 @@ +package com.eatsfine.eatsfine.global.config; + +import com.eatsfine.eatsfine.global.auth.CustomAccessDeniedHandler; +import com.eatsfine.eatsfine.global.auth.CustomAuthenticationEntryPoint; +import com.eatsfine.eatsfine.global.config.jwt.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpMethod; +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.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +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; + +import java.time.Duration; +import java.util.List; + +import static org.springframework.security.config.Customizer.withDefaults; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CustomAuthenticationEntryPoint authenticationEntryPoint; + private final CustomAccessDeniedHandler accessDeniedHandler; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .cors(withDefaults()) + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling(exceptions -> exceptions + .authenticationEntryPoint(authenticationEntryPoint) + .accessDeniedHandler(accessDeniedHandler) + ) + .authorizeHttpRequests(auth -> auth + // preflight은 항상 허용 + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + + // 공개 리소스 / 인증 없이 + .requestMatchers( + "/api/auth/**", + "/swagger-ui.html", + "/swagger-ui/**", + "/v3/api-docs/**", + "/api/v1/deploy/health-check", + "/swagger-resources/**" + ).permitAll() + + .requestMatchers("/auth/**", "/login", "/signup").permitAll() + + // 그 외는 인증 필요 + .anyRequest().authenticated() + ) + + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { // cors 설정 + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOriginPatterns(List.of("*")); // 운영 환경에서는 정확한 도메인만 명시 + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); + config.setAllowedHeaders(List.of("*")); + config.setExposedHeaders(List.of("Authorization")); + config.setAllowCredentials(true); + config.setMaxAge(Duration.ofHours(1)); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java index 9f6ce7bf..dffeb0dc 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java @@ -32,10 +32,8 @@ protected void doFilterInternal(HttpServletRequest request, System.out.println("요청 URI: " + uri); // 인증 없이 통과시킬 경로들 - if (uri.startsWith("/api/v1/auth/login") || - uri.startsWith("/api/v1/auth/register") || - uri.startsWith("/api/v1/auth/reissue") || - uri.startsWith("/user/reissue") || + if (uri.startsWith("/api/auth/login") || + uri.startsWith("/api/auth/signup") || uri.startsWith("/oauth2") || uri.startsWith("/login")) { chain.doFilter(request, response); From dfc0983b61bf0b6ef2163abce3ae929af89c8783 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 30 Jan 2026 03:43:45 +0900 Subject: [PATCH 20/83] =?UTF-8?q?[Feat]=20Refresh=20Token=20=EC=BF=A0?= =?UTF-8?q?=ED=82=A4=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/auth/AuthCookieProvider.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/main/java/com/eatsfine/eatsfine/global/auth/AuthCookieProvider.java diff --git a/src/main/java/com/eatsfine/eatsfine/global/auth/AuthCookieProvider.java b/src/main/java/com/eatsfine/eatsfine/global/auth/AuthCookieProvider.java new file mode 100644 index 00000000..6279705f --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/global/auth/AuthCookieProvider.java @@ -0,0 +1,29 @@ +package com.eatsfine.eatsfine.global.auth; + +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Component +public class AuthCookieProvider { + public ResponseCookie refreshTokenCookie(String refreshToken) { + return ResponseCookie.from("refreshToken", refreshToken) + .httpOnly(true) + .secure(true) + .sameSite("Lax") + .path("/") + .maxAge(Duration.ofDays(14)) + .build(); + } + + public ResponseCookie deleteRefreshTokenCookie() { + return ResponseCookie.from("refreshToken", "") + .httpOnly(true) + .secure(true) + .sameSite("Lax") + .path("/") + .maxAge(0) + .build(); + } +} From d3d6f19b2b3813741bd7f7c2e71a423630aff008 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Wed, 4 Feb 2026 00:03:20 +0900 Subject: [PATCH 21/83] =?UTF-8?q?[Style]=20import=20=EA=B5=AC=EB=AC=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/eatsfine/eatsfine/domain/payment/entity/Payment.java | 2 +- .../com/eatsfine/eatsfine/domain/store/entity/Store.java | 5 ++--- .../eatsfine/domain/tableblock/entity/TableBlock.java | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/entity/Payment.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/entity/Payment.java index 8e2f8f8a..30dc44ef 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/entity/Payment.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/entity/Payment.java @@ -5,7 +5,7 @@ import com.eatsfine.eatsfine.domain.payment.enums.PaymentProvider; import com.eatsfine.eatsfine.domain.payment.enums.PaymentStatus; import com.eatsfine.eatsfine.domain.payment.enums.PaymentType; -import com.eatsfine.eatsfine.global.entity.BaseEntity; +import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java index c0e73d24..5fcf469b 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java @@ -3,14 +3,13 @@ import com.eatsfine.eatsfine.domain.businesshours.entity.BusinessHours; import com.eatsfine.eatsfine.domain.region.entity.Region; import com.eatsfine.eatsfine.domain.store.enums.Category; -import com.eatsfine.eatsfine.domain.store.enums.StoreApprovalStatus; -import com.eatsfine.eatsfine.domain.storetable.entity.StoreTable; import com.eatsfine.eatsfine.domain.table_layout.entity.TableLayout; import com.eatsfine.eatsfine.domain.tableimage.entity.TableImage; import com.eatsfine.eatsfine.domain.user.entity.User; import com.eatsfine.eatsfine.global.apiPayload.code.status.ErrorStatus; import com.eatsfine.eatsfine.global.apiPayload.exception.GeneralException; -import com.eatsfine.eatsfine.global.entity.BaseEntity; +import com.eatsfine.eatsfine.global.common.BaseEntity; + import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/tableblock/entity/TableBlock.java b/src/main/java/com/eatsfine/eatsfine/domain/tableblock/entity/TableBlock.java index f38ab2f8..0c50b23f 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/tableblock/entity/TableBlock.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/tableblock/entity/TableBlock.java @@ -1,7 +1,7 @@ package com.eatsfine.eatsfine.domain.tableblock.entity; import com.eatsfine.eatsfine.domain.storetable.entity.StoreTable; -import com.eatsfine.eatsfine.global.entity.BaseEntity; +import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; From ab72c3acbff628e4c828c3f5afb985c3fda8287e Mon Sep 17 00:00:00 2001 From: SungMinju Date: Wed, 4 Feb 2026 00:04:41 +0900 Subject: [PATCH 22/83] =?UTF-8?q?[Feat]=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/eatsfine/eatsfine/domain/user/entity/User.java | 6 +++++- .../eatsfine/domain/user/repository/UserRepository.java | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java index 375f31fe..818a0f02 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java @@ -40,10 +40,12 @@ public class User extends BaseEntity { @Column(name = "social_type") private SocialType socialType; - @Setter @Column(nullable = true) private String profileImage; + @Column(length = 500) + private String refreshToken; + public void updateNickname(String nickName){ this.nickName = nickName; } @@ -59,4 +61,6 @@ public void updateEmail(String email) { public void updateProfileImage(String profileImage) { this.profileImage = profileImage; } + + public void updateRefreshToken(String refreshToken){this.refreshToken = refreshToken;} } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/repository/UserRepository.java b/src/main/java/com/eatsfine/eatsfine/domain/user/repository/UserRepository.java index b1614ca0..12bca8a5 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/repository/UserRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/repository/UserRepository.java @@ -9,4 +9,5 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); Optional findBySocialTypeAndSocialId(SocialType socialType, String socialId); + boolean existsByEmail(String email); } From af75e6ccc967afa1773294e12d2d1bb1be5ea847 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Wed, 4 Feb 2026 00:04:58 +0900 Subject: [PATCH 23/83] =?UTF-8?q?[Feat]=20=EC=98=A4=EB=A5=98=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/user/status/UserErrorStatus.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java index c27eba75..704609c7 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java @@ -13,9 +13,17 @@ public enum UserErrorStatus implements BaseErrorCode { // 멤버 관련 에러 MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."), NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다."), + EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER4003", "이미 존재하는 이메일입니다."), + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4004", "비밀번호가 올바르지 않습니다."), + + // 토큰 관련 에러 + INVALID_TOKEN(HttpStatus.NOT_FOUND, "TOKEN4001", "토큰이 없습니다."), + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN4002", "토큰이 만료되었습니다."), + REFRESH_TOKEN_NOT_ISSUED(HttpStatus.INTERNAL_SERVER_ERROR, "TOKEN5001", "리프레시 토큰이 발급되지 않았습니다."), + + //이미지 관련 오류 + PROFILE_IMAGE_UPLOAD_NOT_SUPPORTED(HttpStatus.BAD_REQUEST, "PROFILE4001", "프로필 이미지 업로드가 지원되지 않습니다."); - // 토큰 유효 에러 - INVALID_TOKEN(HttpStatus.NOT_FOUND, "TOKEN4001", "토큰이 없습니다."); private final HttpStatus httpStatus; private final String code; From 9578c2de34e05e1c0ef6d517a1cbd0f6e33803aa Mon Sep 17 00:00:00 2001 From: SungMinju Date: Wed, 4 Feb 2026 00:05:19 +0900 Subject: [PATCH 24/83] =?UTF-8?q?[Feat]=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85/=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 102 +++++++++++++++- .../domain/user/service/UserService.java | 20 +++ .../domain/user/service/UserServiceImpl.java | 115 +++++++++++++++++- 3 files changed, 234 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java index b8b749b5..955b0e66 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java @@ -1,9 +1,109 @@ package com.eatsfine.eatsfine.domain.user.controller; + +import com.eatsfine.eatsfine.domain.user.dto.request.UserRequestDto; +import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; +import com.eatsfine.eatsfine.domain.user.exception.UserException; +import com.eatsfine.eatsfine.domain.user.service.UserService; +import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; +import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; +import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; +import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; @RestController @RequiredArgsConstructor public class UserController { + private final UserService userService; + private final JwtTokenProvider jwtTokenProvider; + private final AuthCookieProvider authCookieProvider; + + @PostMapping("/api/auth/signup") + @Operation(summary = "회원가입 API", description = "회원가입을 처리하는 API입니다.") + public ResponseEntity signup(@RequestBody @Valid UserRequestDto.JoinDto joinDto) { + UserResponseDto.JoinResultDto result = userService.signup(joinDto); + return ResponseEntity.ok(result); + } + + @PostMapping("/api/auth/login") + @Operation(summary = "로그인 API", description = "사용자 로그인을 처리하는 API입니다.") + public ResponseEntity> login(@RequestBody UserRequestDto.LoginDto loginDto) { + UserResponseDto.LoginResponseDto loginResult = userService.login(loginDto); + + if (loginResult.getRefreshToken() == null || loginResult.getRefreshToken().isBlank()) { + throw new UserException(UserErrorStatus.REFRESH_TOKEN_NOT_ISSUED); + } + + ResponseCookie refreshCookie = authCookieProvider.refreshTokenCookie(loginResult.getRefreshToken()); + + UserResponseDto.LoginResponseDto body = UserResponseDto.LoginResponseDto.builder() + .id(loginResult.getId()) + .accessToken(loginResult.getAccessToken()) + .refreshToken(null) + .build(); + + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, refreshCookie.toString()) + .body(ApiResponse.onSuccess(body)); + } + + @GetMapping("/api/v1/member/info") + @Operation( + summary = "유저 내 정보 조회 API - 인증 필요", + description = "유저가 내 정보를 조회하는 API입니다.", + security = {@SecurityRequirement(name = "JWT")} + ) + public ApiResponse getMyInfo(HttpServletRequest request) { + return ApiResponse.onSuccess(userService.getMemberInfo(request)); + } + + @PutMapping(value = "/api/v1/member/info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation( + summary = "회원 정보 수정 API - 인증 필요", + description = "회원 정보를 수정하는 API입니다. (프로필 이미지 포함)", + security = {@SecurityRequirement(name = "JWT")} + ) + public ResponseEntity> updateMyInfo( + @RequestPart("updateDto") @Valid UserRequestDto.UpdateDto updateDto, + @RequestPart(value = "profileImage", required = false) MultipartFile profileImage, + HttpServletRequest request + ) { + userService.updateMemberInfo(updateDto, profileImage, request); + return ResponseEntity.ok(ApiResponse.onSuccess("회원 정보가 수정되었습니다.")); + } + + @DeleteMapping("/api/auth/withdraw") + @Operation( + summary = "회원 탈퇴 API - 인증 필요", + description = "회원 탈퇴 기능 API입니다.", + security = {@SecurityRequirement(name = "JWT")} + ) + public ResponseEntity withdraw(HttpServletRequest request) { + userService.withdraw(request); + return ResponseEntity.ok(ApiResponse.onSuccess("회원 탈퇴가 완료되었습니다.")); + } + + @DeleteMapping("/api/auth/logout") + @Operation( + summary = "회원 로그아웃 API - 인증 필요", + description = "회원 로그아웃 기능 API입니다.", + security = {@SecurityRequirement(name = "JWT")} + ) + public ResponseEntity> logout(HttpServletRequest request) { + userService.logout(request); + return ResponseEntity.ok(ApiResponse.onSuccess("로그아웃이 되었습니다.")); + } + } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserService.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserService.java index 6d3ef6b6..1818c909 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserService.java @@ -1,4 +1,24 @@ package com.eatsfine.eatsfine.domain.user.service; +import com.eatsfine.eatsfine.domain.user.dto.request.UserRequestDto; +import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + public interface UserService { + UserResponseDto.JoinResultDto signup(UserRequestDto.JoinDto joinDto); + + UserResponseDto.LoginResponseDto login(UserRequestDto.LoginDto loginDto); + + @Transactional(readOnly = true) + UserResponseDto.UserInfoDto getMemberInfo(HttpServletRequest request); + + @Transactional + String updateMemberInfo(UserRequestDto.UpdateDto updateDto, MultipartFile profileImage, HttpServletRequest request); + + void withdraw(HttpServletRequest request); + + void logout(HttpServletRequest request); + } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java index 96fe2e5f..7fb44484 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java @@ -1,10 +1,121 @@ package com.eatsfine.eatsfine.domain.user.service; +import com.eatsfine.eatsfine.domain.user.converter.UserConverter; +import com.eatsfine.eatsfine.domain.user.dto.request.UserRequestDto; +import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; +import com.eatsfine.eatsfine.domain.user.entity.User; +import com.eatsfine.eatsfine.domain.user.exception.UserException; +import com.eatsfine.eatsfine.domain.user.repository.UserRepository; +import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; +import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; @Service @RequiredArgsConstructor -public class UserServiceImpl { -} +public class UserServiceImpl implements UserService{ + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; + + @Override + public UserResponseDto.JoinResultDto signup(UserRequestDto.JoinDto joinDto) { + // 1) 이메일 중복 체크 + if (userRepository.existsByEmail(joinDto.getEmail())) { + throw new UserException(UserErrorStatus.EMAIL_ALREADY_EXISTS); + } + + // 2) 비밀번호 인코딩 후 유저 생성 + String encoded = passwordEncoder.encode(joinDto.getPassword()); + User user = UserConverter.toUser(joinDto, encoded); + + // 3) 저장 및 응답 + User saved = userRepository.save(user); + return UserConverter.toJoinResult(saved); + } + + @Override + @Transactional + public UserResponseDto.LoginResponseDto login(UserRequestDto.LoginDto loginDto) { + // 1) 사용자 조회 + User user = userRepository.findByEmail(loginDto.getEmail()) + .orElseThrow(() -> new UserException(UserErrorStatus.MEMBER_NOT_FOUND)); + + // 2) 비밀번호 검증 + if (!passwordEncoder.matches(loginDto.getPassword(), user.getPassword())) { + throw new UserException(UserErrorStatus.INVALID_PASSWORD); + } + + // 3) 토큰 발급 + String accessToken = jwtTokenProvider.createAccessToken(user.getEmail()); + String refreshToken = jwtTokenProvider.createRefreshToken(user.getEmail()); + + // 4) refreshToken 저장 + user.updateRefreshToken(refreshToken); + + return UserResponseDto.LoginResponseDto.builder() + .id(user.getId()) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + @Override + @Transactional + public UserResponseDto.UserInfoDto getMemberInfo(HttpServletRequest request) { + User user = getCurrentUser(request); + return UserConverter.toUserInfo(user); + } + + @Override + public String updateMemberInfo(UserRequestDto.UpdateDto updateDto, + MultipartFile profileImage, + HttpServletRequest request) { + User user = getCurrentUser(request); + + if (updateDto.getNickName() != null && !updateDto.getNickName().isBlank()) { + user.updateNickname(updateDto.getNickName()); + } + if (updateDto.getPhoneNumber() != null && !updateDto.getPhoneNumber().isBlank()) { + user.updatePhoneNumber(updateDto.getPhoneNumber()); + } + if (profileImage != null && !profileImage.isEmpty()) { + throw new UserException(UserErrorStatus.PROFILE_IMAGE_UPLOAD_NOT_SUPPORTED); + } + + return "회원 정보가 수정되었습니다."; + } + + @Override + public void withdraw(HttpServletRequest request) { + User user = getCurrentUser(request); + + user.updateRefreshToken(null); + + userRepository.delete(user); + } + + @Override + public void logout(HttpServletRequest request) { + User user = getCurrentUser(request); + + user.updateRefreshToken(null); + } + + private User getCurrentUser(HttpServletRequest request) { + String token = JwtTokenProvider.resolveToken(request); + if (token == null || token.isBlank() || !jwtTokenProvider.validateToken(token)) { + throw new UserException(UserErrorStatus.INVALID_TOKEN); + } + + String email = jwtTokenProvider.getEmailFromToken(token); + + return userRepository.findByEmail(email) + .orElseThrow(() -> new UserException(UserErrorStatus.MEMBER_NOT_FOUND)); + } +} \ No newline at end of file From 379112a359c71f696953441ab45503a43cf90221 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Wed, 4 Feb 2026 01:42:30 +0900 Subject: [PATCH 25/83] =?UTF-8?q?Merge=20=EA=B3=BC=EC=A0=95=20=EC=A4=91=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=83=9D=EB=9E=B5=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/store/entity/Store.java | 79 +++++++++++++++---- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java index 3717efdc..61d4b728 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java @@ -1,7 +1,11 @@ package com.eatsfine.eatsfine.domain.store.entity; import com.eatsfine.eatsfine.domain.businesshours.entity.BusinessHours; +import com.eatsfine.eatsfine.domain.businesshours.exception.BusinessHoursException; +import com.eatsfine.eatsfine.domain.businesshours.status.BusinessHoursErrorStatus; +import com.eatsfine.eatsfine.domain.menu.entity.Menu; import com.eatsfine.eatsfine.domain.region.entity.Region; +import com.eatsfine.eatsfine.domain.store.dto.StoreReqDto; import com.eatsfine.eatsfine.domain.store.enums.Category; import com.eatsfine.eatsfine.domain.store.enums.DepositRate; import com.eatsfine.eatsfine.domain.table_layout.entity.TableLayout; @@ -9,13 +13,16 @@ import com.eatsfine.eatsfine.domain.user.entity.User; import com.eatsfine.eatsfine.global.apiPayload.code.status.ErrorStatus; import com.eatsfine.eatsfine.global.apiPayload.exception.GeneralException; -import com.eatsfine.eatsfine.global.entity.BaseEntity; + +import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.BatchSize; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.DayOfWeek; +import java.time.LocalTime; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -62,7 +69,7 @@ public class Store extends BaseEntity { private String address; @Column(name = "main_image_url") - private String mainImageUrl; + private String mainImageKey; @Builder.Default @Column(name = "rating", precision = 2, scale = 1, nullable = false) @@ -76,9 +83,6 @@ public class Store extends BaseEntity { @Column(name = "booking_interval_minutes", nullable = false) private int bookingIntervalMinutes = 30; - @Column(name = "min_price", nullable = false) - private int minPrice; - @Enumerated(EnumType.STRING) @Column(name = "deposit_rate", nullable = false) private DepositRate depositRate; @@ -87,13 +91,16 @@ public class Store extends BaseEntity { @OneToMany(mappedBy = "store", cascade = CascadeType.ALL, orphanRemoval = true) private List businessHours = new ArrayList<>(); + @Builder.Default + @BatchSize(size = 100) + @OneToMany(mappedBy = "store", cascade = CascadeType.ALL) + private List menus = new ArrayList<>(); + + @Builder.Default @OneToMany(mappedBy = "store", cascade = CascadeType.ALL, orphanRemoval = true) private List tableImages = new ArrayList<>(); - // StoreTable이 아닌 TableLayout 엔티티 참조 -// @OneToMany(mappedBy = "store") -// private List storeTables = new ArrayList<>(); @Builder.Default @OneToMany(mappedBy = "store") @@ -109,6 +116,22 @@ public void removeBusinessHours(BusinessHours businessHours) { businessHours.assignStore(null); } + // 영업시간 변경 + public void updateBusinessHours(DayOfWeek dayOfWeek, LocalTime open, LocalTime close, boolean isClosed) { + BusinessHours businessHours = this.businessHours.stream() + .filter(bh -> bh.getDayOfWeek() == dayOfWeek) + .findFirst() + .orElseThrow(() -> new BusinessHoursException(BusinessHoursErrorStatus._BUSINESS_HOURS_DAY_NOT_FOUND)); + + businessHours.update(open, close, isClosed); + } + + // 메뉴 추가 + public void addMenu(Menu menu) { + this.menus.add(menu); + menu.assignStore(this); + } + public void addTableImage(TableImage tableImage) { this.tableImages.add(tableImage); tableImage.assignStore(this); @@ -119,6 +142,11 @@ public void removeTableImage(TableImage tableImage) { tableImage.assignStore(null); } + // 가게 메인 이미지 등록 + public void updateMainImageKey(String mainImageKey) { + this.mainImageKey = mainImageKey; + } + // 특정 요일의 영업시간 조회 메서드 public BusinessHours getBusinessHoursByDay(DayOfWeek dayOfWeek) { return this.businessHours.stream() @@ -135,12 +163,33 @@ public Optional findBusinessHoursByDay(DayOfWeek dayOfWeek) { .findFirst(); } - public BigDecimal calculateDepositAmount() { - return BigDecimal.valueOf(minPrice) - .multiply(BigDecimal.valueOf(depositRate.getPercent())) - .divide(BigDecimal.valueOf(100), 0, RoundingMode.DOWN); - } - // StoreTable에 대한 연관관계 편의 메서드는 추후 추가 예정 + // 가게 기본 정보 변경 메서드 + public void updateBasicInfo(StoreReqDto.StoreUpdateDto dto) { + if(dto.storeName() != null) { + this.storeName = dto.storeName(); + } + + if(dto.description() != null) { + this.description = dto.description(); + } + + if(dto.phoneNumber() != null) { + this.phoneNumber = dto.phoneNumber(); + } + + if(dto.category() != null) { + this.category = dto.category(); + } + + + if(dto.depositRate() != null) { + this.depositRate = dto.depositRate(); + } + + if(dto.bookingIntervalMinutes() != null) { + this.bookingIntervalMinutes = dto.bookingIntervalMinutes(); + } + } -} +} \ No newline at end of file From 6e49016ce7c20c5dc2df718a81aa937df526adf5 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Wed, 4 Feb 2026 01:43:01 +0900 Subject: [PATCH 26/83] =?UTF-8?q?[Chore]=20BaseEntity=20import=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/eatsfine/eatsfine/domain/booking/entity/Booking.java | 2 ++ .../java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java | 3 ++- .../eatsfine/eatsfine/domain/storetable/entity/StoreTable.java | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/booking/entity/Booking.java b/src/main/java/com/eatsfine/eatsfine/domain/booking/entity/Booking.java index d617055c..ed73e900 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/booking/entity/Booking.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/booking/entity/Booking.java @@ -8,7 +8,9 @@ import com.eatsfine.eatsfine.domain.payment.exception.PaymentException; import com.eatsfine.eatsfine.domain.payment.status.PaymentErrorStatus; import com.eatsfine.eatsfine.domain.store.entity.Store; +import com.eatsfine.eatsfine.domain.storetable.entity.StoreTable; import com.eatsfine.eatsfine.domain.user.entity.User; + import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java index 83119ee0..3c38e435 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/entity/Menu.java @@ -2,7 +2,8 @@ import com.eatsfine.eatsfine.domain.menu.enums.MenuCategory; import com.eatsfine.eatsfine.domain.store.entity.Store; -import com.eatsfine.eatsfine.global.entity.BaseEntity; + +import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.SQLDelete; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/storetable/entity/StoreTable.java b/src/main/java/com/eatsfine/eatsfine/domain/storetable/entity/StoreTable.java index ec8978d4..2eaca4c1 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/storetable/entity/StoreTable.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/storetable/entity/StoreTable.java @@ -2,6 +2,7 @@ import com.eatsfine.eatsfine.domain.storetable.enums.SeatsType; import com.eatsfine.eatsfine.domain.table_layout.entity.TableLayout; + import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; From e4604fa466cc204146e58766cafea0e4486e8853 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Wed, 4 Feb 2026 02:27:27 +0900 Subject: [PATCH 27/83] =?UTF-8?q?[Fix]=20cors=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-local.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index ef35d7cf..fd69063b 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -23,3 +23,17 @@ spring: properties: hibernate: format_sql: true + +payment: + toss: + widget-secret-key: test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6 + +cloud: + aws: + region: ap-northeast-2 + s3: + bucket: eatsfine-images + base-url: https://eatsfine-images.s3.ap-northeast-2.amazonaws.com + +jwt: + secret: ${SECRET_KEY} From 09e7154bbcbcadcbb1b14a62dfb2b1b0cb455b90 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Wed, 4 Feb 2026 02:27:39 +0900 Subject: [PATCH 28/83] =?UTF-8?q?[Fix]=20cors=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/eatsfine/eatsfine/global/config/SecurityConfig.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java index 8b1a15fd..ced45b45 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java @@ -34,8 +34,9 @@ public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http - .cors(withDefaults()) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .exceptionHandling(exceptions -> exceptions @@ -73,6 +74,7 @@ public CorsConfigurationSource corsConfigurationSource() { // cors 설정 config.setAllowedOriginPatterns(List.of("*")); // 운영 환경에서는 정확한 도메인만 명시 config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); config.setAllowedHeaders(List.of("*")); + config.setExposedHeaders(List.of("Authorization", "Set-Cookie")); //쿠키, Authorization 헤더 노출 config.setExposedHeaders(List.of("Authorization")); config.setAllowCredentials(true); config.setMaxAge(Duration.ofHours(1)); @@ -82,6 +84,7 @@ public CorsConfigurationSource corsConfigurationSource() { // cors 설정 return source; } + @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); From 2b5ff867b6ef7e219faae3fdad60ba950f772569 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Wed, 4 Feb 2026 02:28:18 +0900 Subject: [PATCH 29/83] =?UTF-8?q?[Feat]=20=EC=97=90=EB=9F=AC=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/image/status/ImageErrorStatus.java | 1 + .../eatsfine/domain/user/status/UserErrorStatus.java | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/image/status/ImageErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/image/status/ImageErrorStatus.java index cfd97401..a024e442 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/image/status/ImageErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/image/status/ImageErrorStatus.java @@ -15,6 +15,7 @@ public enum ImageErrorStatus implements BaseErrorCode { _IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "IMAGE404", "해당하는 이미지가 존재하지 않습니다."), _INVALID_IMAGE_KEY(HttpStatus.BAD_REQUEST, "IMAGE4003", "유효하지 않은 이미지 키입니다."), _INVALID_S3_DIRECTORY(HttpStatus.BAD_REQUEST, "IMAGE4004", "유효하지 않은 S3 디렉토리입니다."), + FILE_TOO_LARGE(HttpStatus.BAD_REQUEST, "IMAGE4005", "첨부하는 이미지의 크기가 너무 큽니다.") ; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java index 704609c7..c6a19078 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java @@ -19,10 +19,7 @@ public enum UserErrorStatus implements BaseErrorCode { // 토큰 관련 에러 INVALID_TOKEN(HttpStatus.NOT_FOUND, "TOKEN4001", "토큰이 없습니다."), EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN4002", "토큰이 만료되었습니다."), - REFRESH_TOKEN_NOT_ISSUED(HttpStatus.INTERNAL_SERVER_ERROR, "TOKEN5001", "리프레시 토큰이 발급되지 않았습니다."), - - //이미지 관련 오류 - PROFILE_IMAGE_UPLOAD_NOT_SUPPORTED(HttpStatus.BAD_REQUEST, "PROFILE4001", "프로필 이미지 업로드가 지원되지 않습니다."); + REFRESH_TOKEN_NOT_ISSUED(HttpStatus.INTERNAL_SERVER_ERROR, "TOKEN5001", "리프레시 토큰이 발급되지 않았습니다."); private final HttpStatus httpStatus; From 43162653a62a9dfa79222c9ec3da1f5da2279096 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Wed, 4 Feb 2026 02:30:28 +0900 Subject: [PATCH 30/83] =?UTF-8?q?[Feat]=20S3=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/service/UserServiceImpl.java | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java index 7fb44484..72ab36b7 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java @@ -1,6 +1,8 @@ package com.eatsfine.eatsfine.domain.user.service; +import com.eatsfine.eatsfine.domain.image.exception.ImageException; +import com.eatsfine.eatsfine.domain.image.status.ImageErrorStatus; import com.eatsfine.eatsfine.domain.user.converter.UserConverter; import com.eatsfine.eatsfine.domain.user.dto.request.UserRequestDto; import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; @@ -9,19 +11,25 @@ import com.eatsfine.eatsfine.domain.user.repository.UserRepository; import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; +import com.eatsfine.eatsfine.global.s3.S3Service; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.web.multipart.MultipartFile; +@Slf4j @Service @RequiredArgsConstructor public class UserServiceImpl implements UserService{ private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final JwtTokenProvider jwtTokenProvider; + private final S3Service s3Service; @Override public UserResponseDto.JoinResultDto signup(UserRequestDto.JoinDto joinDto) { @@ -73,24 +81,66 @@ public UserResponseDto.UserInfoDto getMemberInfo(HttpServletRequest request) { } @Override + @Transactional public String updateMemberInfo(UserRequestDto.UpdateDto updateDto, MultipartFile profileImage, HttpServletRequest request) { + User user = getCurrentUser(request); + //닉네임/전화번호 부분 수정 if (updateDto.getNickName() != null && !updateDto.getNickName().isBlank()) { user.updateNickname(updateDto.getNickName()); } if (updateDto.getPhoneNumber() != null && !updateDto.getPhoneNumber().isBlank()) { user.updatePhoneNumber(updateDto.getPhoneNumber()); } + + //프로필 이미지 부분 수정 (파일이 들어온 경우에만) if (profileImage != null && !profileImage.isEmpty()) { - throw new UserException(UserErrorStatus.PROFILE_IMAGE_UPLOAD_NOT_SUPPORTED); - } + validateProfileImage(profileImage); + + String oldKey = user.getProfileImage(); + + String directory = "users/profile/" + user.getId(); + + String newKey = s3Service.upload(profileImage, directory); + + user.updateProfileImage(newKey); + + // 기존 이미지가 있었으면 삭제 + if (oldKey != null && !oldKey.isBlank()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + try { + s3Service.deleteByKey(oldKey); + } catch (Exception e) { + log.warn("이전 프로필 이미지를 삭제하는 데 실패했습니다. oldKey={}", oldKey, e); + } + } + }); + } + } return "회원 정보가 수정되었습니다."; } + private void validateProfileImage(MultipartFile file) { + String contentType = file.getContentType(); + if (contentType == null || !contentType.startsWith("image/")) { + throw new ImageException(ImageErrorStatus.INVALID_FILE_TYPE); + } + + // 용량 제한 (5MB) + long maxBytes = 5L * 1024 * 1024; + if (file.getSize() > maxBytes) { + throw new ImageException(ImageErrorStatus.FILE_TOO_LARGE); + } + } + + + @Override public void withdraw(HttpServletRequest request) { User user = getCurrentUser(request); From f360a5746484a85ac3d026342c123b18ccc56123 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Wed, 4 Feb 2026 02:30:53 +0900 Subject: [PATCH 31/83] =?UTF-8?q?[Fix]=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B3=80=EC=88=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/user/dto/request/UserRequestDto.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java index 64dc61fe..023883e3 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java @@ -63,7 +63,6 @@ public static class LoginDto { @Getter @Setter public static class UpdateDto { - private String profileImage; private String email; private String nickName; private String phoneNumber; From 6d46ef5b76703904ddf0a2c04ffc214db679c26e Mon Sep 17 00:00:00 2001 From: SungMinju Date: Wed, 4 Feb 2026 03:33:26 +0900 Subject: [PATCH 32/83] =?UTF-8?q?[Fix]=20=ED=9A=8C=EC=9B=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95=20API=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 39 ++++++++++++++----- .../domain/user/service/UserServiceImpl.java | 23 +++++++++-- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java index 955b0e66..1f6ee1ba 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java @@ -10,7 +10,10 @@ import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestBody; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import jakarta.servlet.http.HttpServletRequest; @@ -23,6 +26,9 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; + +@Tag(name = "User", description = "회원 관리 API") +@Slf4j @RestController @RequiredArgsConstructor public class UserController { @@ -69,21 +75,36 @@ public ApiResponse getMyInfo(HttpServletRequest req return ApiResponse.onSuccess(userService.getMemberInfo(request)); } - @PutMapping(value = "/api/v1/member/info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PatchMapping(value = "/api/v1/member/info") @Operation( - summary = "회원 정보 수정 API - 인증 필요", - description = "회원 정보를 수정하는 API입니다. (프로필 이미지 포함)", + summary = "닉네임/전화번호 수정 API - 인증 필요", + description = "닉네임/전화번호만 수정합니다. (JSON)", security = {@SecurityRequirement(name = "JWT")} ) - public ResponseEntity> updateMyInfo( - @RequestPart("updateDto") @Valid UserRequestDto.UpdateDto updateDto, - @RequestPart(value = "profileImage", required = false) MultipartFile profileImage, + public ResponseEntity> updateMyInfoText( + @RequestBody @Valid UserRequestDto.UpdateDto updateDto, HttpServletRequest request + ) { + String result = userService.updateMemberInfo(updateDto, null, request); + return ResponseEntity.ok(ApiResponse.onSuccess(result)); + } + + @PutMapping( + value = "/api/v1/member/profile-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation( + summary = "프로필 이미지 수정 API - 인증 필요", + description = "프로필 이미지만 수정합니다. (multipart/form-data)", + security = {@SecurityRequirement(name = "JWT")} + ) + public ResponseEntity> updateProfileImage( + @RequestPart(value = "profileImage") MultipartFile profileImage, HttpServletRequest request ) { - userService.updateMemberInfo(updateDto, profileImage, request); - return ResponseEntity.ok(ApiResponse.onSuccess("회원 정보가 수정되었습니다.")); + String result = userService.updateMemberInfo(null, profileImage, request); + return ResponseEntity.ok(ApiResponse.onSuccess(result)); } + + @DeleteMapping("/api/auth/withdraw") @Operation( summary = "회원 탈퇴 API - 인증 필요", diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java index 72ab36b7..de5b703b 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java @@ -88,26 +88,28 @@ public String updateMemberInfo(UserRequestDto.UpdateDto updateDto, User user = getCurrentUser(request); + boolean changed = false; + //닉네임/전화번호 부분 수정 if (updateDto.getNickName() != null && !updateDto.getNickName().isBlank()) { user.updateNickname(updateDto.getNickName()); + changed = true; } if (updateDto.getPhoneNumber() != null && !updateDto.getPhoneNumber().isBlank()) { user.updatePhoneNumber(updateDto.getPhoneNumber()); + changed = true; } //프로필 이미지 부분 수정 (파일이 들어온 경우에만) if (profileImage != null && !profileImage.isEmpty()) { - validateProfileImage(profileImage); String oldKey = user.getProfileImage(); - String directory = "users/profile/" + user.getId(); - String newKey = s3Service.upload(profileImage, directory); user.updateProfileImage(newKey); + changed = true; // 기존 이미지가 있었으면 삭제 if (oldKey != null && !oldKey.isBlank()) { @@ -123,6 +125,21 @@ public void afterCommit() { }); } } + + if (!changed) { + log.info("[Service] No changes detected. userId={}", user.getId()); + return "변경된 내용이 없습니다."; + } + + userRepository.save(user); + userRepository.flush(); + + log.info("[Service] Updated userId={}, nickname={}, phone={}, profileKey={}", + user.getId(), + user.getNickName(), + user.getPhoneNumber(), + user.getProfileImage()); + return "회원 정보가 수정되었습니다."; } From 5a4ff0c645d0e6bfdd09a6d163eb3744c3a9886e Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 5 Feb 2026 02:15:01 +0900 Subject: [PATCH 33/83] =?UTF-8?q?[Fix]=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=A6=AC=EB=B7=B0=20=EC=88=98=EC=A0=95=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/eatsfine/eatsfine/domain/term/entity/Term.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java b/src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java index 3bc78a0f..4934fed2 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java @@ -19,7 +19,7 @@ public class Term extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @OneToOne(fetch = FetchType.LAZY) + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true) @JoinColumn(name = "user_id", nullable = false, unique = true) private User user; From cb5902f67f8cecf9627b62f1345a291dfe5f0c70 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 5 Feb 2026 03:47:15 +0900 Subject: [PATCH 34/83] =?UTF-8?q?[Feat]=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20Term(=EC=95=BD=EA=B4=80)=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/eatsfine/eatsfine/domain/term/entity/Term.java | 7 ++----- .../domain/term/repository/TermRepository.java | 10 ++++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/term/repository/TermRepository.java diff --git a/src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java b/src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java index 4934fed2..cf0f4970 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java @@ -23,14 +23,11 @@ public class Term extends BaseEntity { @JoinColumn(name = "user_id", nullable = false, unique = true) private User user; - @Builder.Default @Column(name = "tos_consent", nullable = false) - private Boolean tosConsent = true; + private Boolean tosConsent; - @Builder.Default @Column(name = "privacy_consent", nullable = false) - private Boolean privacyConsent = true; - + private Boolean privacyConsent; @Column(name = "marketing_consent", nullable = false) private Boolean marketingConsent; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/term/repository/TermRepository.java b/src/main/java/com/eatsfine/eatsfine/domain/term/repository/TermRepository.java new file mode 100644 index 00000000..1c1c8d7f --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/term/repository/TermRepository.java @@ -0,0 +1,10 @@ +package com.eatsfine.eatsfine.domain.term.repository; + +import com.eatsfine.eatsfine.domain.term.entity.Term; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TermRepository extends JpaRepository { + +} From dcbac9632d57a8c2648147136ea0ff19b00ee623 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 5 Feb 2026 03:48:22 +0900 Subject: [PATCH 35/83] =?UTF-8?q?[Refactor]=20@PasswordMatch=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=EA=B8=B0=20=EB=B2=94=EC=9A=A9=EC=84=B1=20=ED=99=95?= =?UTF-8?q?=EB=8C=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/eatsfine/global/auth/AuthCookieProvider.java | 8 ++++++-- .../global/validator/annotation/PasswordMatch.java | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/global/auth/AuthCookieProvider.java b/src/main/java/com/eatsfine/eatsfine/global/auth/AuthCookieProvider.java index 6279705f..2807b4a1 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/auth/AuthCookieProvider.java +++ b/src/main/java/com/eatsfine/eatsfine/global/auth/AuthCookieProvider.java @@ -8,6 +8,10 @@ @Component public class AuthCookieProvider { public ResponseCookie refreshTokenCookie(String refreshToken) { + if (refreshToken == null || refreshToken.isBlank()) { + throw new IllegalArgumentException("refreshToken must not be blank"); + } + return ResponseCookie.from("refreshToken", refreshToken) .httpOnly(true) .secure(true) @@ -17,13 +21,13 @@ public ResponseCookie refreshTokenCookie(String refreshToken) { .build(); } - public ResponseCookie deleteRefreshTokenCookie() { + public ResponseCookie clearRefreshTokenCookie() { return ResponseCookie.from("refreshToken", "") .httpOnly(true) .secure(true) .sameSite("Lax") .path("/") - .maxAge(0) + .maxAge(0) // 수명을 0으로 설정하여 즉시 삭제 .build(); } } diff --git a/src/main/java/com/eatsfine/eatsfine/global/validator/annotation/PasswordMatch.java b/src/main/java/com/eatsfine/eatsfine/global/validator/annotation/PasswordMatch.java index 14190ba7..d9f16cce 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/validator/annotation/PasswordMatch.java +++ b/src/main/java/com/eatsfine/eatsfine/global/validator/annotation/PasswordMatch.java @@ -12,9 +12,10 @@ @Retention(RetentionPolicy.RUNTIME) public @interface PasswordMatch { String message() default "비밀번호와 비밀번호 확인이 일치하지 않습니다."; - Class[] groups() default {}; - Class[] payload() default {}; + String passwordField() default "password"; + String confirmField() default "passwordConfirm"; + } From ac07dbce1a0d324eaf4709187114c0c5feaaad45 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 5 Feb 2026 03:48:47 +0900 Subject: [PATCH 36/83] =?UTF-8?q?[Refactor]=20@PasswordMatch=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=EA=B8=B0=20=EB=B2=94=EC=9A=A9=EC=84=B1=20=ED=99=95?= =?UTF-8?q?=EB=8C=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../valid/PasswordMatchValidator.java | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/global/validator/valid/PasswordMatchValidator.java b/src/main/java/com/eatsfine/eatsfine/global/validator/valid/PasswordMatchValidator.java index 175073bd..6fd130c6 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/validator/valid/PasswordMatchValidator.java +++ b/src/main/java/com/eatsfine/eatsfine/global/validator/valid/PasswordMatchValidator.java @@ -1,26 +1,38 @@ package com.eatsfine.eatsfine.global.validator.valid; -import com.eatsfine.eatsfine.domain.user.dto.request.UserRequestDto; + import com.eatsfine.eatsfine.global.validator.annotation.PasswordMatch; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; +import org.springframework.beans.BeanWrapperImpl; + +public class PasswordMatchValidator implements ConstraintValidator { -public class PasswordMatchValidator implements ConstraintValidator { + private String passwordFieldName; + private String confirmFieldName; @Override - public boolean isValid(UserRequestDto.JoinDto dto, ConstraintValidatorContext context) { - if (dto.getPassword() == null || dto.getPasswordConfirm() == null) { - context.disableDefaultConstraintViolation(); - context.buildConstraintViolationWithTemplate("비밀번호와 비밀번호 확인은 필수입니다.") - .addPropertyNode("passwordConfirm") - .addConstraintViolation(); - return false; + public void initialize(PasswordMatch constraintAnnotation) { + // 어노테이션에서 정한 필드 이름을 가져옴 + this.passwordFieldName = constraintAnnotation.passwordField(); + this.confirmFieldName = constraintAnnotation.confirmField(); + } + + @Override + public boolean isValid(Object dto, ConstraintValidatorContext context) { + Object passwordValue = getFieldValue(dto, passwordFieldName); + Object confirmValue = getFieldValue(dto, confirmFieldName); + + // 둘 다 null이면 검증 패스 + if (passwordValue == null || confirmValue == null) { + return true; } - if (!dto.getPassword().equals(dto.getPasswordConfirm())) { + // 값 비교 + if (!passwordValue.equals(confirmValue)) { context.disableDefaultConstraintViolation(); - context.buildConstraintViolationWithTemplate("비밀번호와 비밀번호 확인이 일치하지 않습니다.") - .addPropertyNode("passwordConfirm") // 이 필드에 오류 표시 + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) + .addPropertyNode(confirmFieldName) .addConstraintViolation(); return false; } @@ -28,4 +40,12 @@ public boolean isValid(UserRequestDto.JoinDto dto, ConstraintValidatorContext co return true; } -} + // 필드 값을 안전하게 가져오는 헬퍼 메서드 + private Object getFieldValue(Object object, String fieldName) { + try { + return new BeanWrapperImpl(object).getPropertyValue(fieldName); + } catch (Exception e) { + return null; + } + } +} \ No newline at end of file From 22a93d2347d5f046159e96751bd76145968bd0b9 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 5 Feb 2026 03:49:45 +0900 Subject: [PATCH 37/83] =?UTF-8?q?[Feat]=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20Term(=EC=95=BD=EA=B4=80)=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/user/converter/UserConverter.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java index 824fae7b..81c3c0d0 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java @@ -1,5 +1,6 @@ package com.eatsfine.eatsfine.domain.user.converter; +import com.eatsfine.eatsfine.domain.term.entity.Term; import com.eatsfine.eatsfine.domain.user.dto.request.UserRequestDto; import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; import com.eatsfine.eatsfine.domain.user.entity.User; @@ -72,6 +73,15 @@ public static User toUser(UserRequestDto.JoinDto dto, String encodedPassword) { .build(); } + public static Term toUserTerm(UserRequestDto.JoinDto dto, User user) { + return Term.builder() + .user(user) // 생성된 유저와 매핑 + .tosConsent(dto.getTosConsent()) // 서비스 이용약관 동의 + .privacyConsent(dto.getPrivacyConsent()) // 개인정보 처리방침 동의 + .marketingConsent(dto.getMarketingConsent()) // 마케팅 수신 동의 + .build(); + } + /* 소셜 유저 생성 (최초 소셜 가입 등) From 931b6ac0c1f45e5dbce23dfde0a14b6198b69138 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 5 Feb 2026 03:50:52 +0900 Subject: [PATCH 38/83] =?UTF-8?q?[Fix]=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=A1=A4=EB=B0=B1=20?= =?UTF-8?q?=EC=8B=9C=20S3=20=ED=8C=8C=EC=9D=BC=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/service/UserServiceImpl.java | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java index de5b703b..49706461 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java @@ -3,6 +3,7 @@ import com.eatsfine.eatsfine.domain.image.exception.ImageException; import com.eatsfine.eatsfine.domain.image.status.ImageErrorStatus; +import com.eatsfine.eatsfine.domain.term.repository.TermRepository; import com.eatsfine.eatsfine.domain.user.converter.UserConverter; import com.eatsfine.eatsfine.domain.user.dto.request.UserRequestDto; import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; @@ -27,11 +28,13 @@ @RequiredArgsConstructor public class UserServiceImpl implements UserService{ private final UserRepository userRepository; + private final TermRepository termRepository; private final PasswordEncoder passwordEncoder; private final JwtTokenProvider jwtTokenProvider; private final S3Service s3Service; @Override + @Transactional public UserResponseDto.JoinResultDto signup(UserRequestDto.JoinDto joinDto) { // 1) 이메일 중복 체크 if (userRepository.existsByEmail(joinDto.getEmail())) { @@ -41,10 +44,13 @@ public UserResponseDto.JoinResultDto signup(UserRequestDto.JoinDto joinDto) { // 2) 비밀번호 인코딩 후 유저 생성 String encoded = passwordEncoder.encode(joinDto.getPassword()); User user = UserConverter.toUser(joinDto, encoded); + User savedUser = userRepository.save(user); + + // 3) 약관 동의 내역 저장 + termRepository.save(UserConverter.toUserTerm(joinDto, savedUser)); - // 3) 저장 및 응답 - User saved = userRepository.save(user); - return UserConverter.toJoinResult(saved); + // 4) 응답 반환 + return UserConverter.toJoinResult(savedUser); } @Override @@ -106,18 +112,36 @@ public String updateMemberInfo(UserRequestDto.UpdateDto updateDto, String oldKey = user.getProfileImage(); String directory = "users/profile/" + user.getId(); + + // S3에 먼저 업로드 String newKey = s3Service.upload(profileImage, directory); user.updateProfileImage(newKey); changed = true; - // 기존 이미지가 있었으면 삭제 + // 트랜잭션 롤백 시 방금 올린 새 파일 삭제 (S3 고아 파일 방지) + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCompletion(int status) { + if (status == STATUS_ROLLED_BACK) { + try { + s3Service.deleteByKey(newKey); + log.info("트랜잭션 롤백으로 인해 업로드된 새 이미지를 삭제했습니다. key={}", newKey); + } catch (Exception e) { + log.error("롤백 후 새 이미지 삭제 실패. key={}", newKey, e); + } + } + } + }); + + // 트랜잭션 커밋 성공 시 기존(옛날) 파일 삭제 if (oldKey != null && !oldKey.isBlank()) { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { try { s3Service.deleteByKey(oldKey); + log.info("프로필 수정 완료 후 이전 이미지를 삭제했습니다. oldKey={}", oldKey); } catch (Exception e) { log.warn("이전 프로필 이미지를 삭제하는 데 실패했습니다. oldKey={}", oldKey, e); } @@ -133,7 +157,7 @@ public void afterCommit() { userRepository.save(user); userRepository.flush(); - + log.info("[Service] Updated userId={}, nickname={}, phone={}, profileKey={}", user.getId(), user.getNickName(), @@ -157,17 +181,26 @@ private void validateProfileImage(MultipartFile file) { } - @Override + @Transactional public void withdraw(HttpServletRequest request) { User user = getCurrentUser(request); - user.updateRefreshToken(null); + String profileImage = user.getProfileImage(); + if (profileImage != null && !profileImage.isBlank()) { + try { + s3Service.deleteByKey(profileImage); + } catch (Exception e) { + log.warn("프로필 이미지 삭제 실패. key={}", profileImage, e); + } + } + user.updateRefreshToken(null); userRepository.delete(user); } @Override + @Transactional public void logout(HttpServletRequest request) { User user = getCurrentUser(request); From 6f6b2a5e765f3c59f57e089072ff23cac956ef2d Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 5 Feb 2026 03:51:48 +0900 Subject: [PATCH 39/83] =?UTF-8?q?[Fix]=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=8B=9C=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=BF=A0=ED=82=A4=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/controller/UserController.java | 10 +++++++--- .../eatsfine/global/config/SecurityConfig.java | 1 - .../global/config/properties/JwtProperties.java | 4 ++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java index 1f6ee1ba..91e28b39 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java @@ -10,7 +10,6 @@ import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestBody; @@ -75,6 +74,7 @@ public ApiResponse getMyInfo(HttpServletRequest req return ApiResponse.onSuccess(userService.getMemberInfo(request)); } + @PatchMapping(value = "/api/v1/member/info") @Operation( summary = "닉네임/전화번호 수정 API - 인증 필요", @@ -88,6 +88,7 @@ public ResponseEntity> updateMyInfoText( return ResponseEntity.ok(ApiResponse.onSuccess(result)); } + @PutMapping( value = "/api/v1/member/profile-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation( @@ -104,7 +105,6 @@ public ResponseEntity> updateProfileImage( } - @DeleteMapping("/api/auth/withdraw") @Operation( summary = "회원 탈퇴 API - 인증 필요", @@ -116,6 +116,7 @@ public ResponseEntity withdraw(HttpServletRequest request) { return ResponseEntity.ok(ApiResponse.onSuccess("회원 탈퇴가 완료되었습니다.")); } + @DeleteMapping("/api/auth/logout") @Operation( summary = "회원 로그아웃 API - 인증 필요", @@ -124,7 +125,10 @@ public ResponseEntity withdraw(HttpServletRequest request) { ) public ResponseEntity> logout(HttpServletRequest request) { userService.logout(request); - return ResponseEntity.ok(ApiResponse.onSuccess("로그아웃이 되었습니다.")); + ResponseCookie clearCookie = authCookieProvider.clearRefreshTokenCookie(); + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, clearCookie.toString()) + .body(ApiResponse.onSuccess("로그아웃이 되었습니다.")); } } diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java index ced45b45..f55d5cc4 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java @@ -75,7 +75,6 @@ public CorsConfigurationSource corsConfigurationSource() { // cors 설정 config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); config.setAllowedHeaders(List.of("*")); config.setExposedHeaders(List.of("Authorization", "Set-Cookie")); //쿠키, Authorization 헤더 노출 - config.setExposedHeaders(List.of("Authorization")); config.setAllowCredentials(true); config.setMaxAge(Duration.ofHours(1)); diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/properties/JwtProperties.java b/src/main/java/com/eatsfine/eatsfine/global/config/properties/JwtProperties.java index c81b91ab..dd85c4fb 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/properties/JwtProperties.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/properties/JwtProperties.java @@ -1,15 +1,19 @@ package com.eatsfine.eatsfine.global.config.properties; +import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; @Component @Getter @Setter +@Validated @ConfigurationProperties("jwt") public class JwtProperties { + @NotBlank private String secret; } \ No newline at end of file From 592b436a4ba80f5e52026a3409b83e98444edad4 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 5 Feb 2026 03:52:42 +0900 Subject: [PATCH 40/83] =?UTF-8?q?[Refactor]=20@PasswordMatch=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=EA=B8=B0=20=EB=B2=94=EC=9A=A9=EC=84=B1=20=ED=99=95?= =?UTF-8?q?=EB=8C=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java index 190a1887..c8da9390 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java @@ -25,7 +25,7 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep String password = user.getPassword(); if (password == null) { - password = ""; + throw new UsernameNotFoundException("비밀번호 기반 로그인 대상이 아닙니다."); } return new User(user.getEmail(), password, List.of()); From dc56c8c8938f11aebd75501b1fc227824ce3e5e6 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 5 Feb 2026 03:53:12 +0900 Subject: [PATCH 41/83] =?UTF-8?q?[Refactor]=20@PasswordMatch=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=EA=B8=B0=20=EB=B2=94=EC=9A=A9=EC=84=B1=20=ED=99=95?= =?UTF-8?q?=EB=8C=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/dto/request/UserRequestDto.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java index 023883e3..e368ada3 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java @@ -2,10 +2,7 @@ import com.eatsfine.eatsfine.global.validator.annotation.PasswordMatch; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.*; import lombok.Getter; import lombok.Setter; @@ -36,11 +33,11 @@ public static class JoinDto{ @NotBlank(message = "비밀번호 확인은 필수입니다.") private String passwordConfirm; // 비밀번호 확인 - @NotNull(message = "이용약관에 동의합니다.") + @AssertTrue(message = "이용약관에 동의해야 합니다.") @Schema(description = "서비스 이용약관 동의 여부 (필수)", example = "true") private Boolean tosConsent; - @NotNull(message = "개인정보 처리방침에 동의합니다") + @AssertTrue(message = "개인정보 처리방침에 동의해야 합니다.") @Schema(description = "개인정보 수집 및 이용 동의 여부 (필수)", example = "true") private Boolean privacyConsent; @@ -69,7 +66,7 @@ public static class UpdateDto { } @Getter - @PasswordMatch + @PasswordMatch(passwordField = "newPassword", confirmField = "newPasswordConfirm") public static class ChangePasswordDto { @NotBlank(message = "현재 비밀번호는 필수입니다.") From 65c36adfc95b848362bef81e52660102d4103b83 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 5 Feb 2026 04:05:55 +0900 Subject: [PATCH 42/83] =?UTF-8?q?[Chore]=20=EC=9C=A0=EC=A0=80=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/user/controller/UserController.java | 2 +- .../domain/user/service/{ => userService}/UserService.java | 2 +- .../user/service/{ => userService}/UserServiceImpl.java | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) rename src/main/java/com/eatsfine/eatsfine/domain/user/service/{ => userService}/UserService.java (93%) rename src/main/java/com/eatsfine/eatsfine/domain/user/service/{ => userService}/UserServiceImpl.java (97%) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java index 91e28b39..1bb127a1 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java @@ -4,7 +4,7 @@ import com.eatsfine.eatsfine.domain.user.dto.request.UserRequestDto; import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; import com.eatsfine.eatsfine.domain.user.exception.UserException; -import com.eatsfine.eatsfine.domain.user.service.UserService; +import com.eatsfine.eatsfine.domain.user.service.userService.UserService; import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserService.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java similarity index 93% rename from src/main/java/com/eatsfine/eatsfine/domain/user/service/UserService.java rename to src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java index 1818c909..6c81360d 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java @@ -1,4 +1,4 @@ -package com.eatsfine.eatsfine.domain.user.service; +package com.eatsfine.eatsfine.domain.user.service.userService; import com.eatsfine.eatsfine.domain.user.dto.request.UserRequestDto; import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java similarity index 97% rename from src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java rename to src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java index 49706461..738de43d 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java @@ -1,4 +1,4 @@ -package com.eatsfine.eatsfine.domain.user.service; +package com.eatsfine.eatsfine.domain.user.service.userService; import com.eatsfine.eatsfine.domain.image.exception.ImageException; @@ -10,6 +10,7 @@ import com.eatsfine.eatsfine.domain.user.entity.User; import com.eatsfine.eatsfine.domain.user.exception.UserException; import com.eatsfine.eatsfine.domain.user.repository.UserRepository; +import com.eatsfine.eatsfine.domain.user.service.userService.UserService; import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; import com.eatsfine.eatsfine.global.s3.S3Service; @@ -26,7 +27,7 @@ @Slf4j @Service @RequiredArgsConstructor -public class UserServiceImpl implements UserService{ +public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final TermRepository termRepository; private final PasswordEncoder passwordEncoder; From 8d48aa2ee4c31e4ecab63bbd8333dca5323edd7d Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 5 Feb 2026 04:34:59 +0900 Subject: [PATCH 43/83] =?UTF-8?q?[Feat]=20=EC=86=8C=EC=85=9C=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/enums/SocialType.java | 1 - .../CustomOAuth2MemberServiceImpl.java | 60 +++++++++++++++++++ .../oauthService/Oauth2MemberService.java | 8 +++ .../oauthService/Oauth2MemberServiceImpl.java | 33 ++++++++++ 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/CustomOAuth2MemberServiceImpl.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberService.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberServiceImpl.java diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/enums/SocialType.java b/src/main/java/com/eatsfine/eatsfine/domain/user/enums/SocialType.java index a0ef11b6..c9dd89ce 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/enums/SocialType.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/enums/SocialType.java @@ -1,7 +1,6 @@ package com.eatsfine.eatsfine.domain.user.enums; public enum SocialType { - NAVER, KAKAO, GOOGLE } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/CustomOAuth2MemberServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/CustomOAuth2MemberServiceImpl.java new file mode 100644 index 00000000..fb677f65 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/CustomOAuth2MemberServiceImpl.java @@ -0,0 +1,60 @@ +package com.eatsfine.eatsfine.domain.user.service.oauthService; + +import com.eatsfine.eatsfine.domain.user.enums.SocialType; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class CustomOAuth2MemberServiceImpl extends DefaultOAuth2UserService { + + private final Oauth2MemberService oauth2MemberService; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User oAuth2User = super.loadUser(userRequest); + + String provider = userRequest.getClientRegistration().getRegistrationId(); + Map attributes = oAuth2User.getAttributes(); + + String providerId = null; + String email = null; + String name = null; + + SocialType socialType = SocialType.valueOf(provider.toUpperCase()); + + try { + if (socialType == SocialType.GOOGLE) { + providerId = (String) attributes.get("sub"); + email = (String) attributes.get("email"); + name = (String) attributes.get("name"); + } else if (socialType == SocialType.KAKAO) { + Map kakaoAccount = (Map) attributes.get("kakao_account"); + providerId = String.valueOf(attributes.get("id")); + if (kakaoAccount != null) { + email = (String) kakaoAccount.get("email"); + Map profile = (Map) kakaoAccount.get("profile"); + if (profile != null) { + name = (String) profile.get("nickname"); + } + } + } + + // 회원 생성/조회 + oauth2MemberService.findOrCreateOauthUser(socialType, providerId, email, name); + + } catch (Exception e) { + OAuth2Error error = new OAuth2Error("invalid_request", e.getMessage(), null); + throw new OAuth2AuthenticationException(error, e); + } + + return oAuth2User; + } +} \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberService.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberService.java new file mode 100644 index 00000000..d4f1f115 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberService.java @@ -0,0 +1,8 @@ +package com.eatsfine.eatsfine.domain.user.service.oauthService; + +import com.eatsfine.eatsfine.domain.user.entity.User; +import com.eatsfine.eatsfine.domain.user.enums.SocialType; + +public interface Oauth2MemberService { + User findOrCreateOauthUser(SocialType socialType, String socialId, String email, String nickName); +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberServiceImpl.java new file mode 100644 index 00000000..193c2658 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberServiceImpl.java @@ -0,0 +1,33 @@ +package com.eatsfine.eatsfine.domain.user.service.oauthService; + + +import com.eatsfine.eatsfine.domain.user.converter.UserConverter; +import com.eatsfine.eatsfine.domain.user.entity.User; +import com.eatsfine.eatsfine.domain.user.enums.SocialType; +import com.eatsfine.eatsfine.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class Oauth2MemberServiceImpl implements Oauth2MemberService { + + private final UserRepository userRepository; + + @Override + public User findOrCreateOauthUser(SocialType socialType, String socialId, String email, String nickName) { + return userRepository.findBySocialTypeAndSocialId(socialType, socialId) + .orElseGet(() -> createSocialUser(socialType, socialId, email, nickName)); + } + private User createSocialUser(SocialType socialType, String socialId, String email, String nickName) { + //소셜 로그인 시 전화번호가 없을 경우 임시 값 설정 + String defaultPhoneNumber = "000-0000-0000"; + + User newUser = UserConverter.toSocialUser(email, nickName, defaultPhoneNumber, socialId, socialType); + + return userRepository.save(newUser); + } + +} From 1a1fd2f788e98d2ec139984235bafb1f0fbf4f77 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 5 Feb 2026 14:31:15 +0900 Subject: [PATCH 44/83] =?UTF-8?q?[Feat]=20refreshToken=20=EC=9E=AC?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthController.java | 41 +++++++++++++++++ .../domain/user/converter/UserConverter.java | 2 +- .../user/dto/request/UserRequestDto.java | 1 + .../user/dto/response/UserResponseDto.java | 6 +++ .../domain/user/exception/AuthException.java | 11 +++++ .../service/authService/AuthTokenService.java | 8 ++++ .../authService/AuthTokenServiceImpl.java | 40 +++++++++++++++++ .../domain/user/status/AuthErrorStatus.java | 44 +++++++++++++++++++ .../domain/user/status/UserErrorStatus.java | 6 +-- 9 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/exception/AuthException.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenService.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java new file mode 100644 index 00000000..38d07f11 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java @@ -0,0 +1,41 @@ +package com.eatsfine.eatsfine.domain.user.controller; + +import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; +import com.eatsfine.eatsfine.domain.user.service.authService.AuthTokenService; +import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; +import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/auth") +public class AuthController { + + private final AuthTokenService authTokenService; + private final AuthCookieProvider authCookieProvider; + + @PostMapping("/reissue") + public ResponseEntity> reissue( + @CookieValue(value = "refreshToken", required = false) String refreshToken, + HttpServletResponse response + ) { + // 서비스에서 검증 &재발급 + AuthTokenService.ReissueResult result = authTokenService.reissue(refreshToken); + + // refresh 쿠키 갱신 + ResponseCookie refreshCookie = authCookieProvider.refreshTokenCookie(result.refreshToken()); + response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString()); + + // access 응답 + return ResponseEntity.ok( + ApiResponse.onSuccess(new UserResponseDto.AccessTokenResponse(result.accessToken())) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java index 81c3c0d0..1af78cc7 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java @@ -75,7 +75,7 @@ public static User toUser(UserRequestDto.JoinDto dto, String encodedPassword) { public static Term toUserTerm(UserRequestDto.JoinDto dto, User user) { return Term.builder() - .user(user) // 생성된 유저와 매핑 + .user(user) .tosConsent(dto.getTosConsent()) // 서비스 이용약관 동의 .privacyConsent(dto.getPrivacyConsent()) // 개인정보 처리방침 동의 .marketingConsent(dto.getMarketingConsent()) // 마케팅 수신 동의 diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java index e368ada3..c8ea733a 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java @@ -3,6 +3,7 @@ import com.eatsfine.eatsfine.global.validator.annotation.PasswordMatch; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java index a81b0338..5253ed9f 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java @@ -63,4 +63,10 @@ public static class UpdatePasswordDto { private String message; } + @Getter + @AllArgsConstructor + public static class AccessTokenResponse { + private String accessToken; + } + } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/AuthException.java b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/AuthException.java new file mode 100644 index 00000000..ae9ec968 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/AuthException.java @@ -0,0 +1,11 @@ +package com.eatsfine.eatsfine.domain.user.exception; + + +import com.eatsfine.eatsfine.global.apiPayload.code.BaseErrorCode; +import com.eatsfine.eatsfine.global.apiPayload.exception.GeneralException; + +public class AuthException extends GeneralException { + public AuthException(BaseErrorCode code) { + super(code); + } +} \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenService.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenService.java new file mode 100644 index 00000000..963b70cf --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenService.java @@ -0,0 +1,8 @@ +package com.eatsfine.eatsfine.domain.user.service.authService; + +public interface AuthTokenService { + + ReissueResult reissue(String refreshToken); + + record ReissueResult(String accessToken, String refreshToken) {} +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java new file mode 100644 index 00000000..63c2a94a --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java @@ -0,0 +1,40 @@ +package com.eatsfine.eatsfine.domain.user.service.authService; + +import com.eatsfine.eatsfine.domain.user.exception.AuthException; +import com.eatsfine.eatsfine.domain.user.status.AuthErrorStatus; +import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class AuthTokenServiceImpl implements AuthTokenService { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + public ReissueResult reissue(String refreshToken) { + + if (refreshToken == null || refreshToken.isBlank()) { + throw new AuthException(AuthErrorStatus.INVALID_TOKEN); + } + + // validateToken이 만료/위조를 구분 못하면 일단 INVALID로 처리 + if (!jwtTokenProvider.validateToken(refreshToken)) { + throw new AuthException(AuthErrorStatus.INVALID_TOKEN); + } + + String subject = jwtTokenProvider.getEmailFromToken(refreshToken); + if (subject == null || subject.isBlank()) { + throw new AuthException(AuthErrorStatus.INVALID_TOKEN); + } + + // 새 토큰 발급 + String newAccessToken = jwtTokenProvider.createAccessToken(subject); + String newRefreshToken = jwtTokenProvider.createRefreshToken(subject); + + return new ReissueResult(newAccessToken, newRefreshToken); + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java new file mode 100644 index 00000000..a3818292 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java @@ -0,0 +1,44 @@ +package com.eatsfine.eatsfine.domain.user.status; + +import com.eatsfine.eatsfine.global.apiPayload.code.BaseErrorCode; +import com.eatsfine.eatsfine.global.apiPayload.code.ErrorReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum AuthErrorStatus implements BaseErrorCode { + + OAUTH2_EMAIL_NOT_FOUND(HttpStatus.BAD_REQUEST, "AUTH4001", "소셜 로그인 이메일을 가져올 수 없습니다."), + OAUTH2_PROVIDER_NOT_SUPPORTED(HttpStatus.BAD_REQUEST, "AUTH4002", "지원하지 않는 소셜 로그인 제공자입니다."), + REFRESH_TOKEN_MISSING(HttpStatus.UNAUTHORIZED, "AUTH4005", "리프레시 토큰이 없습니다."), + + + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH4003", "유효하지 않은 토큰입니다."), + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH4004", "토큰이 만료되었습니다."), + REFRESH_TOKEN_NOT_ISSUED(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH5001", "리프레시 토큰이 발급되지 않았습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ErrorReasonDto getReason() { + return ErrorReasonDto.builder() + .isSuccess(false) + .message(message) + .code(code) + .build(); + } + + @Override + public ErrorReasonDto getReasonHttpStatus() { + return ErrorReasonDto.builder() + .httpStatus(httpStatus) + .isSuccess(false) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java index c6a19078..6a00cbcd 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java @@ -44,8 +44,4 @@ public ErrorReasonDto getReasonHttpStatus() { .message(message) .build(); } - - - - - } +} From e39b7b591d13aad1753bbd8a29d3c9244d848474 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 5 Feb 2026 14:31:46 +0900 Subject: [PATCH 45/83] =?UTF-8?q?[Feat]=20=EC=86=8C=EC=85=9C=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/CustomOAuth2FailureHandler.java | 32 ++++++ .../handler/CustomOAuth2SuccessHandler.java | 103 ++++++++++++++++++ .../oauthService/Oauth2MemberServiceImpl.java | 31 +++++- .../global/config/SecurityConfig.java | 15 +++ 4 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java new file mode 100644 index 00000000..ce3bfa70 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java @@ -0,0 +1,32 @@ +package com.eatsfine.eatsfine.domain.user.exception.handler; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; + +@Component +public class CustomOAuth2FailureHandler implements AuthenticationFailureHandler { + + // TODO: 환경별로 분리 추천 (application.yml) + private static final String ERROR_REDIRECT_BASE = "https://chicchic-mu.vercel.app/login/error"; + + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + + String redirectUrl = UriComponentsBuilder + .fromUriString(ERROR_REDIRECT_BASE) + .queryParam("error", "oauth2_login_failed") + .build() + .toUriString(); + + response.sendRedirect(redirectUrl); + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java new file mode 100644 index 00000000..8c5327df --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java @@ -0,0 +1,103 @@ +package com.eatsfine.eatsfine.domain.user.exception.handler; + +import com.eatsfine.eatsfine.domain.user.entity.User; +import com.eatsfine.eatsfine.domain.user.enums.SocialType; +import com.eatsfine.eatsfine.domain.user.repository.UserRepository; +import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; +import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class CustomOAuth2SuccessHandler implements AuthenticationSuccessHandler { + + private final UserRepository userRepository; + private final JwtTokenProvider jwtTokenProvider; + private final AuthCookieProvider authCookieProvider; + + private static final String CALLBACK_REDIRECT_BASE = "https://chicchic-mu.vercel.app/oauth/callback"; + private static final String LOGIN_ERROR_REDIRECT_BASE = "https://chicchic-mu.vercel.app/login/error"; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + + OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication; + + String provider = oauthToken.getAuthorizedClientRegistrationId(); + SocialType socialType = SocialType.valueOf(provider.toUpperCase()); + + OAuth2User oAuth2User = oauthToken.getPrincipal(); + String socialId = extractProviderId(socialType, oAuth2User); + + if (socialId == null || socialId.isBlank()) { + redirectFail(response, "social_id_not_found"); + return; + } + + User user = userRepository.findBySocialTypeAndSocialId(socialType, socialId) + .orElse(null); + + if (user == null) { + redirectFail(response, "user_not_found_after_oauth2"); + return; + } + + String subject = String.valueOf(user.getId()); + + String accessToken = jwtTokenProvider.createAccessToken(subject); + String refreshToken = jwtTokenProvider.createRefreshToken(subject); + + ResponseCookie refreshCookie = authCookieProvider.refreshTokenCookie(refreshToken); + response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString()); + + String redirectUrl = UriComponentsBuilder + .fromUriString(CALLBACK_REDIRECT_BASE) + .queryParam("accessToken", accessToken) + .build() + .toUriString(); + + response.sendRedirect(redirectUrl); + } + + private void redirectFail(HttpServletResponse response, String error) throws IOException { + String failUrl = UriComponentsBuilder + .fromUriString(LOGIN_ERROR_REDIRECT_BASE) + .queryParam("error", error) + .build() + .toUriString(); + response.sendRedirect(failUrl); + } + + @SuppressWarnings("unchecked") + private String extractProviderId(SocialType socialType, OAuth2User oAuth2User) { + Map attributes = oAuth2User.getAttributes(); + + if (socialType == SocialType.GOOGLE) { + Object sub = attributes.get("sub"); + return sub != null ? String.valueOf(sub) : null; + } + + if (socialType == SocialType.KAKAO) { + Object id = attributes.get("id"); + return id != null ? String.valueOf(id) : null; + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberServiceImpl.java index 193c2658..1efb583a 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberServiceImpl.java @@ -6,9 +6,13 @@ import com.eatsfine.eatsfine.domain.user.enums.SocialType; import com.eatsfine.eatsfine.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + +@Slf4j @Service @Transactional @RequiredArgsConstructor @@ -18,14 +22,33 @@ public class Oauth2MemberServiceImpl implements Oauth2MemberService { @Override public User findOrCreateOauthUser(SocialType socialType, String socialId, String email, String nickName) { + // 1. 소셜 ID로 이미 가입된 회원이 있는지 조회 return userRepository.findBySocialTypeAndSocialId(socialType, socialId) - .orElseGet(() -> createSocialUser(socialType, socialId, email, nickName)); + .orElseGet(() -> findByEmailOrJoin(socialType, socialId, email, nickName)); } + + private User findByEmailOrJoin(SocialType socialType, String socialId, String email, String nickName) { + // 소셜 ID는 없지만, "같은 이메일"을 쓰는 기존 회원이 있는지 조회 + Optional existingUser = userRepository.findByEmail(email); + + if (existingUser.isPresent()) { + // 이미 가입된 이메일이 있음 -> 계정 연동 (소셜 정보만 업데이트) + User user = existingUser.get(); + log.info("기존 회원 계정 연동: email={}, socialType={}", email, socialType); + + user.linkSocial(socialType, socialId); + return user; + } + + // 아예 처음 온 회원 -> 신규 회원가입 + return createSocialUser(socialType, socialId, email, nickName); + } + private User createSocialUser(SocialType socialType, String socialId, String email, String nickName) { - //소셜 로그인 시 전화번호가 없을 경우 임시 값 설정 - String defaultPhoneNumber = "000-0000-0000"; + log.info("신규 소셜 회원 가입: email={}", email); - User newUser = UserConverter.toSocialUser(email, nickName, defaultPhoneNumber, socialId, socialType); + // 전화번호에 null 전달 + User newUser = UserConverter.toSocialUser(email, nickName, null, socialId, socialType); return userRepository.save(newUser); } diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java index f55d5cc4..3f66af06 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java @@ -1,5 +1,8 @@ package com.eatsfine.eatsfine.global.config; +import com.eatsfine.eatsfine.domain.user.exception.handler.CustomOAuth2FailureHandler; +import com.eatsfine.eatsfine.domain.user.exception.handler.CustomOAuth2SuccessHandler; +import com.eatsfine.eatsfine.domain.user.service.oauthService.CustomOAuth2MemberServiceImpl; import com.eatsfine.eatsfine.global.auth.CustomAccessDeniedHandler; import com.eatsfine.eatsfine.global.auth.CustomAuthenticationEntryPoint; import com.eatsfine.eatsfine.global.config.jwt.JwtAuthenticationFilter; @@ -31,6 +34,10 @@ public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CustomAuthenticationEntryPoint authenticationEntryPoint; private final CustomAccessDeniedHandler accessDeniedHandler; + private final CustomOAuth2MemberServiceImpl customOAuth2UserService; + private final CustomOAuth2SuccessHandler customOAuth2SuccessHandler; + private final CustomOAuth2FailureHandler customOAuth2FailureHandler; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -62,6 +69,14 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 그 외는 인증 필요 .anyRequest().authenticated() ) + .oauth2Login(oauth2 -> oauth2 + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuth2UserService) + ) + .successHandler(customOAuth2SuccessHandler) + .failureHandler(customOAuth2FailureHandler) + + ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); From 4fa3dff792628ca953cfc3024560e15ab9b948b5 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 5 Feb 2026 14:32:55 +0900 Subject: [PATCH 46/83] =?UTF-8?q?[Fix]=20=EA=B0=9C=EC=9D=B8=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=B3=80=EA=B2=BD=20=EC=BD=94=EB=93=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 --- .../eatsfine/eatsfine/domain/user/entity/User.java | 11 +++++++---- .../user/service/userService/UserServiceImpl.java | 8 +++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java index 818a0f02..d628643f 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java @@ -27,7 +27,7 @@ public class User extends BaseEntity { private String password; - @Column(nullable = false, length = 20) + @Column(nullable = true, length = 20) private String phoneNumber; @Enumerated(EnumType.STRING) @@ -54,13 +54,16 @@ public void updatePhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } - public void updateEmail(String email) { - this.email = email; - } + public void updateEmail(String email) {this.email = email;} public void updateProfileImage(String profileImage) { this.profileImage = profileImage; } public void updateRefreshToken(String refreshToken){this.refreshToken = refreshToken;} + + public void linkSocial(SocialType socialType, String socialId) { + this.socialType = socialType; + this.socialId = socialId; + } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java index 738de43d..5ccf66d3 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java @@ -98,9 +98,11 @@ public String updateMemberInfo(UserRequestDto.UpdateDto updateDto, boolean changed = false; //닉네임/전화번호 부분 수정 - if (updateDto.getNickName() != null && !updateDto.getNickName().isBlank()) { - user.updateNickname(updateDto.getNickName()); - changed = true; + if (updateDto != null) { + if (updateDto.getNickName() != null && !updateDto.getNickName().isBlank()) { + user.updateNickname(updateDto.getNickName()); + changed = true; + } } if (updateDto.getPhoneNumber() != null && !updateDto.getPhoneNumber().isBlank()) { user.updatePhoneNumber(updateDto.getPhoneNumber()); From 3937bd5167475eccd271b3d7203d45ff504a3b61 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 5 Feb 2026 14:33:25 +0900 Subject: [PATCH 47/83] =?UTF-8?q?[Chore]=20Valid=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/user/controller/UserController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java index 1bb127a1..d103cf93 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java @@ -44,7 +44,7 @@ public ResponseEntity signup(@RequestBody @Valid @PostMapping("/api/auth/login") @Operation(summary = "로그인 API", description = "사용자 로그인을 처리하는 API입니다.") - public ResponseEntity> login(@RequestBody UserRequestDto.LoginDto loginDto) { + public ResponseEntity> login(@RequestBody @Valid UserRequestDto.LoginDto loginDto) { UserResponseDto.LoginResponseDto loginResult = userService.login(loginDto); if (loginResult.getRefreshToken() == null || loginResult.getRefreshToken().isBlank()) { From a86fc8ea60d8426bd55ad621dbdd5eb5041d6fa7 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 6 Feb 2026 00:06:52 +0900 Subject: [PATCH 48/83] =?UTF-8?q?[Feat]=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20OA?= =?UTF-8?q?uth2=20=EC=84=B1=EA=B3=B5/=EC=8B=A4=ED=8C=A8=20=ED=95=B8?= =?UTF-8?q?=EB=93=A4=EB=9F=AC=20=EB=B0=8F=20=EB=A6=AC=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EB=A0=89=ED=8A=B8=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/CustomOAuth2FailureHandler.java | 2 +- .../handler/CustomOAuth2SuccessHandler.java | 65 ++++++++++--------- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java index ce3bfa70..8adbf781 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java @@ -14,7 +14,7 @@ public class CustomOAuth2FailureHandler implements AuthenticationFailureHandler { // TODO: 환경별로 분리 추천 (application.yml) - private static final String ERROR_REDIRECT_BASE = "https://chicchic-mu.vercel.app/login/error"; + private static final String ERROR_REDIRECT_BASE = "https://eatsfine.co.kr/login/error"; @Override public void onAuthenticationFailure(HttpServletRequest request, diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java index 8c5327df..be79d1b5 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java @@ -16,6 +16,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; @@ -23,51 +24,59 @@ @Component @RequiredArgsConstructor +@Transactional public class CustomOAuth2SuccessHandler implements AuthenticationSuccessHandler { private final UserRepository userRepository; private final JwtTokenProvider jwtTokenProvider; private final AuthCookieProvider authCookieProvider; - private static final String CALLBACK_REDIRECT_BASE = "https://chicchic-mu.vercel.app/oauth/callback"; - private static final String LOGIN_ERROR_REDIRECT_BASE = "https://chicchic-mu.vercel.app/login/error"; + private static final String CALLBACK_REDIRECT_BASE = "https://eatsfine.co.kr/oauth/callback"; + private static final String LOGIN_ERROR_REDIRECT_BASE = "https://eatsfine.co.kr/login/error"; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, - Authentication authentication) throws IOException, ServletException { + Authentication authentication) throws IOException { OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication; - - String provider = oauthToken.getAuthorizedClientRegistrationId(); + String provider = oauthToken.getAuthorizedClientRegistrationId(); // google, kakao SocialType socialType = SocialType.valueOf(provider.toUpperCase()); OAuth2User oAuth2User = oauthToken.getPrincipal(); - String socialId = extractProviderId(socialType, oAuth2User); - if (socialId == null || socialId.isBlank()) { - redirectFail(response, "social_id_not_found"); + String email = extractEmail(socialType, oAuth2User); + if (email == null || email.isBlank()) { + String failUrl = UriComponentsBuilder.fromUriString(LOGIN_ERROR_REDIRECT_BASE) + .queryParam("error", "email_not_found") + .build().toUriString(); + response.sendRedirect(failUrl); return; } - User user = userRepository.findBySocialTypeAndSocialId(socialType, socialId) - .orElse(null); - + // DB에서 user 조회 + User user = userRepository.findByEmail(email).orElse(null); if (user == null) { - redirectFail(response, "user_not_found_after_oauth2"); + String failUrl = UriComponentsBuilder.fromUriString(LOGIN_ERROR_REDIRECT_BASE) + .queryParam("error", "user_not_found") + .build().toUriString(); + response.sendRedirect(failUrl); return; } - String subject = String.valueOf(user.getId()); + // 토큰 발급 + String accessToken = jwtTokenProvider.createAccessToken(email); + String refreshToken = jwtTokenProvider.createRefreshToken(email); - String accessToken = jwtTokenProvider.createAccessToken(subject); - String refreshToken = jwtTokenProvider.createRefreshToken(subject); + // refresh DB 저장 + user.updateRefreshToken(refreshToken); + userRepository.save(user); + // refresh 쿠키 세팅 ResponseCookie refreshCookie = authCookieProvider.refreshTokenCookie(refreshToken); response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString()); - String redirectUrl = UriComponentsBuilder - .fromUriString(CALLBACK_REDIRECT_BASE) + String redirectUrl = UriComponentsBuilder.fromUriString(CALLBACK_REDIRECT_BASE) .queryParam("accessToken", accessToken) .build() .toUriString(); @@ -75,27 +84,21 @@ public void onAuthenticationSuccess(HttpServletRequest request, response.sendRedirect(redirectUrl); } - private void redirectFail(HttpServletResponse response, String error) throws IOException { - String failUrl = UriComponentsBuilder - .fromUriString(LOGIN_ERROR_REDIRECT_BASE) - .queryParam("error", error) - .build() - .toUriString(); - response.sendRedirect(failUrl); - } - @SuppressWarnings("unchecked") - private String extractProviderId(SocialType socialType, OAuth2User oAuth2User) { + private String extractEmail(SocialType socialType, OAuth2User oAuth2User) { Map attributes = oAuth2User.getAttributes(); if (socialType == SocialType.GOOGLE) { - Object sub = attributes.get("sub"); - return sub != null ? String.valueOf(sub) : null; + Object email = attributes.get("email"); + return email != null ? String.valueOf(email) : null; } if (socialType == SocialType.KAKAO) { - Object id = attributes.get("id"); - return id != null ? String.valueOf(id) : null; + Object kakaoAccountObj = attributes.get("kakao_account"); + if (kakaoAccountObj instanceof Map kakaoAccount) { + Object email = kakaoAccount.get("email"); + return email != null ? String.valueOf(email) : null; + } } return null; From 22c2b7411b18eddfeb1249f799b7956b6f7960af Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 6 Feb 2026 00:07:35 +0900 Subject: [PATCH 49/83] =?UTF-8?q?[Feat]=20refreshToken=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20accessToken=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20API=20?= =?UTF-8?q?=EB=B0=8F=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthController.java | 4 +--- .../authService/AuthTokenServiceImpl.java | 24 ++++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java index 38d07f11..91dd8da2 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java @@ -2,6 +2,7 @@ import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; import com.eatsfine.eatsfine.domain.user.service.authService.AuthTokenService; +import com.eatsfine.eatsfine.domain.user.service.userService.UserService; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; import jakarta.servlet.http.HttpServletResponse; @@ -26,14 +27,11 @@ public ResponseEntity> reissue( @CookieValue(value = "refreshToken", required = false) String refreshToken, HttpServletResponse response ) { - // 서비스에서 검증 &재발급 AuthTokenService.ReissueResult result = authTokenService.reissue(refreshToken); - // refresh 쿠키 갱신 ResponseCookie refreshCookie = authCookieProvider.refreshTokenCookie(result.refreshToken()); response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString()); - // access 응답 return ResponseEntity.ok( ApiResponse.onSuccess(new UserResponseDto.AccessTokenResponse(result.accessToken())) ); diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java index 63c2a94a..008fd3dd 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java @@ -1,6 +1,8 @@ package com.eatsfine.eatsfine.domain.user.service.authService; +import com.eatsfine.eatsfine.domain.user.entity.User; import com.eatsfine.eatsfine.domain.user.exception.AuthException; +import com.eatsfine.eatsfine.domain.user.repository.UserRepository; import com.eatsfine.eatsfine.domain.user.status.AuthErrorStatus; import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; import lombok.RequiredArgsConstructor; @@ -13,6 +15,7 @@ public class AuthTokenServiceImpl implements AuthTokenService { private final JwtTokenProvider jwtTokenProvider; + private final UserRepository userRepository; @Override public ReissueResult reissue(String refreshToken) { @@ -21,20 +24,29 @@ public ReissueResult reissue(String refreshToken) { throw new AuthException(AuthErrorStatus.INVALID_TOKEN); } - // validateToken이 만료/위조를 구분 못하면 일단 INVALID로 처리 if (!jwtTokenProvider.validateToken(refreshToken)) { throw new AuthException(AuthErrorStatus.INVALID_TOKEN); } - String subject = jwtTokenProvider.getEmailFromToken(refreshToken); - if (subject == null || subject.isBlank()) { + String email = jwtTokenProvider.getEmailFromToken(refreshToken); + if (email == null || email.isBlank()) { + throw new AuthException(AuthErrorStatus.INVALID_TOKEN); + } + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new AuthException(AuthErrorStatus.INVALID_TOKEN)); + + // DB에 저장된 refreshToken과 쿠키 refreshToken이 같아야만 재발급 허용 + if (user.getRefreshToken() == null || !user.getRefreshToken().equals(refreshToken)) { throw new AuthException(AuthErrorStatus.INVALID_TOKEN); } // 새 토큰 발급 - String newAccessToken = jwtTokenProvider.createAccessToken(subject); - String newRefreshToken = jwtTokenProvider.createRefreshToken(subject); + String newAccessToken = jwtTokenProvider.createAccessToken(email); + String newRefreshToken = jwtTokenProvider.createRefreshToken(email); + + user.updateRefreshToken(newRefreshToken); return new ReissueResult(newAccessToken, newRefreshToken); } -} +} \ No newline at end of file From 8f15e04d0d7abba59ff7cabb27c4ee552dcb3538 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 6 Feb 2026 00:08:08 +0900 Subject: [PATCH 50/83] =?UTF-8?q?[Feat]=20JWT=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20local=20O?= =?UTF-8?q?Auth=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/auth/AuthCookieProvider.java | 2 ++ .../config/jwt/JwtAuthenticationFilter.java | 1 + src/main/resources/application-local.yml | 34 +++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/src/main/java/com/eatsfine/eatsfine/global/auth/AuthCookieProvider.java b/src/main/java/com/eatsfine/eatsfine/global/auth/AuthCookieProvider.java index 2807b4a1..a5a28e10 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/auth/AuthCookieProvider.java +++ b/src/main/java/com/eatsfine/eatsfine/global/auth/AuthCookieProvider.java @@ -16,6 +16,7 @@ public ResponseCookie refreshTokenCookie(String refreshToken) { .httpOnly(true) .secure(true) .sameSite("Lax") + .domain(".eatsfine.co.kr") .path("/") .maxAge(Duration.ofDays(14)) .build(); @@ -26,6 +27,7 @@ public ResponseCookie clearRefreshTokenCookie() { .httpOnly(true) .secure(true) .sameSite("Lax") + .domain(".eatsfine.co.kr") .path("/") .maxAge(0) // 수명을 0으로 설정하여 즉시 삭제 .build(); diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java index dffeb0dc..30258362 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java @@ -34,6 +34,7 @@ protected void doFilterInternal(HttpServletRequest request, // 인증 없이 통과시킬 경로들 if (uri.startsWith("/api/auth/login") || uri.startsWith("/api/auth/signup") || + uri.startsWith("/api/auth/reissue") || uri.startsWith("/oauth2") || uri.startsWith("/login")) { chain.doFilter(request, response); diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index fd69063b..97dcb9a8 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -23,6 +23,40 @@ spring: properties: hibernate: format_sql: true + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} + scope: + - email + - profile + redirect-uri: "https://eatsfine.co.kr/oauth/login/oauth2/code/google" + authorization-grant-type: authorization_code + kakao: + client-id: ${KAKAO_CLIENT_ID} + client-secret: "" + scope: + - profile_nickname + - profile_image + - account_email + redirect-uri: "https://eatsfine.co.kr/oauth/code/kakao" + authorization-grant-type: authorization_code + client-name: Kakao + provider: kakao + provider: + google: + authorization-uri: https://accounts.google.com/o/oauth2/v2/auth + token-uri: https://oauth2.googleapis.com/token + user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo + 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 + payment: toss: From 7f636a255b61133e0e3726234e75ab73bd1dedec Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 6 Feb 2026 04:06:13 +0900 Subject: [PATCH 51/83] =?UTF-8?q?[Feat]=20OAuth2=20authorization=20request?= =?UTF-8?q?=20=EC=BF=A0=ED=82=A4=20=EC=B2=98=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...eOAuth2AuthorizationRequestRepository.java | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java diff --git a/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java b/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java new file mode 100644 index 00000000..b62c7f88 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java @@ -0,0 +1,117 @@ +package com.eatsfine.eatsfine.global.auth; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.util.SerializationUtils; +import org.springframework.web.util.WebUtils; + +import java.util.Base64; + +public class HttpCookieOAuth2AuthorizationRequestRepository + implements AuthorizationRequestRepository { + + private static final Logger log = LoggerFactory.getLogger(HttpCookieOAuth2AuthorizationRequestRepository.class); + + public static final String OAUTH2_AUTH_REQUEST_COOKIE_NAME = "oauth2_auth_request"; + private static final int COOKIE_EXPIRE_SECONDS = 180; + + @Override + public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { + log.info("[LOAD] 쿠키에서 AuthorizationRequest 로드 시도"); + + // ⭐ 모든 쿠키 이름 출력 + if (request.getCookies() != null) { + for (Cookie c : request.getCookies()) { + log.info("[LOAD] 발견된 쿠키: name={}, value={}, path={}, domain={}", + c.getName(), + c.getValue().length() > 20 ? c.getValue().substring(0, 20) + "..." : c.getValue(), + c.getPath(), + c.getDomain()); + } + } + + Cookie cookie = WebUtils.getCookie(request, OAUTH2_AUTH_REQUEST_COOKIE_NAME); + + if (cookie == null) { + log.warn("[LOAD] '{}' 이름의 쿠키를 찾을 수 없음!", OAUTH2_AUTH_REQUEST_COOKIE_NAME); + return null; + } + + if (cookie.getValue() == null || cookie.getValue().isBlank()) { + log.warn("[LOAD] 쿠키 값이 비어있음"); + return null; + } + + try { + byte[] bytes = Base64.getUrlDecoder().decode(cookie.getValue()); + Object deserialized = SerializationUtils.deserialize(bytes); + log.info("[LOAD] AuthorizationRequest 로드 성공"); + return (deserialized instanceof OAuth2AuthorizationRequest req) ? req : null; + } catch (Exception e) { + log.error("[LOAD] 역직렬화 실패", e); + return null; + } + } + + @Override + public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, + HttpServletRequest request, + HttpServletResponse response) { + if (authorizationRequest == null) { + log.info("[SAVE] authorizationRequest가 null이므로 쿠키 제거"); + removeAuthorizationRequestCookies(response); + return; + } + + try { + byte[] bytes = SerializationUtils.serialize(authorizationRequest); + String value = Base64.getUrlEncoder().encodeToString(bytes); + + Cookie cookie = new Cookie(OAUTH2_AUTH_REQUEST_COOKIE_NAME, value); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(COOKIE_EXPIRE_SECONDS); + + // ⭐ 환경에 따라 설정 + cookie.setSecure(false); // 로컬 HTTP: false, 운영 HTTPS: true + + // ⭐ 도메인 명시 (선택사항, 문제 있을 때만) + // cookie.setDomain("eatsfine.co.kr"); // 운영 환경 + // cookie.setDomain("localhost"); // 로컬 환경 + + response.addCookie(cookie); + + log.info("[SAVE] AuthorizationRequest 쿠키 저장 완료 - name={}, path={}, maxAge={}, secure={}, domain={}", + cookie.getName(), cookie.getPath(), cookie.getMaxAge(), cookie.getSecure(), cookie.getDomain()); + + } catch (Exception e) { + log.error("[SAVE] 직렬화 실패", e); + } + } + + @Override + public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, + HttpServletResponse response) { + log.info("[REMOVE] AuthorizationRequest 제거"); + OAuth2AuthorizationRequest authRequest = loadAuthorizationRequest(request); + removeAuthorizationRequestCookies(response); + return authRequest; + } + + private void removeAuthorizationRequestCookies(HttpServletResponse response) { + Cookie cookie = new Cookie(OAUTH2_AUTH_REQUEST_COOKIE_NAME, ""); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(0); + cookie.setSecure(false); // 저장할 때와 동일하게 + cookie.setAttribute("SameSite", "Lax"); // 저장할 때와 동일하게 + response.addCookie(cookie); + + log.info("[REMOVE] 쿠키 제거 완료"); + } +} \ No newline at end of file From 089716bb1186e646e775723d94a176fddf993da3 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 6 Feb 2026 04:06:59 +0900 Subject: [PATCH 52/83] =?UTF-8?q?[Fix]=20OAuth2=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=BF=A0=ED=82=A4=20=EC=A0=80=EC=9E=A5/=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/CustomOAuth2FailureHandler.java | 12 ++- .../handler/CustomOAuth2SuccessHandler.java | 93 ++++++++++++++++--- .../global/config/SecurityConfig.java | 40 ++++---- 3 files changed, 110 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java index 8adbf781..9b11a92e 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java @@ -3,23 +3,27 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; - @Component public class CustomOAuth2FailureHandler implements AuthenticationFailureHandler { - // TODO: 환경별로 분리 추천 (application.yml) + private static final Logger log = LoggerFactory.getLogger(CustomOAuth2FailureHandler.class); + private static final String ERROR_REDIRECT_BASE = "https://eatsfine.co.kr/login/error"; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, - AuthenticationException exception) throws IOException, ServletException { + AuthenticationException exception) throws IOException { + + log.warn("[OAuth2 FAILURE] uri={}, msg={}", request.getRequestURI(), exception.getMessage(), exception); String redirectUrl = UriComponentsBuilder .fromUriString(ERROR_REDIRECT_BASE) @@ -29,4 +33,4 @@ public void onAuthenticationFailure(HttpServletRequest request, response.sendRedirect(redirectUrl); } -} +} \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java index be79d1b5..5b1fd83b 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java @@ -5,10 +5,11 @@ import com.eatsfine.eatsfine.domain.user.repository.UserRepository; import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; -import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.security.core.Authentication; @@ -27,6 +28,8 @@ @Transactional public class CustomOAuth2SuccessHandler implements AuthenticationSuccessHandler { + private static final Logger log = LoggerFactory.getLogger(CustomOAuth2SuccessHandler.class); + private final UserRepository userRepository; private final JwtTokenProvider jwtTokenProvider; private final AuthCookieProvider authCookieProvider; @@ -41,26 +44,42 @@ public void onAuthenticationSuccess(HttpServletRequest request, OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication; String provider = oauthToken.getAuthorizedClientRegistrationId(); // google, kakao - SocialType socialType = SocialType.valueOf(provider.toUpperCase()); + + SocialType socialType; + try { + socialType = SocialType.valueOf(provider.toUpperCase()); + } catch (IllegalArgumentException e) { + log.error("Unknown provider registrationId={}", provider, e); + redirectFail(response, "unknown_provider"); + return; + } OAuth2User oAuth2User = oauthToken.getPrincipal(); + Map attrs = oAuth2User.getAttributes(); + + // ✅ 디버깅 핵심 로그 + log.info("[OAuth2 SUCCESS] provider={}, attrs={}", provider, attrs); + + // 카카오의 경우 id도 추출해두면 원인 파악에 큰 도움 됨 + String socialId = extractSocialId(socialType, attrs); + log.info("[OAuth2 SUCCESS] provider={}, socialId={}", provider, socialId); + + String email = extractEmail(socialType, attrs); + log.info("[OAuth2 SUCCESS] provider={}, extractedEmail={}", provider, email); - String email = extractEmail(socialType, oAuth2User); if (email == null || email.isBlank()) { - String failUrl = UriComponentsBuilder.fromUriString(LOGIN_ERROR_REDIRECT_BASE) - .queryParam("error", "email_not_found") - .build().toUriString(); - response.sendRedirect(failUrl); + // 카카오 email 관련 상태값도 같이 찍어주면 디버깅 끝남 + if (socialType == SocialType.KAKAO) { + logKakaoAccountStatus(attrs); + } + redirectFail(response, "email_not_found"); return; } // DB에서 user 조회 User user = userRepository.findByEmail(email).orElse(null); if (user == null) { - String failUrl = UriComponentsBuilder.fromUriString(LOGIN_ERROR_REDIRECT_BASE) - .queryParam("error", "user_not_found") - .build().toUriString(); - response.sendRedirect(failUrl); + redirectFail(response, "user_not_found"); return; } @@ -81,13 +100,21 @@ public void onAuthenticationSuccess(HttpServletRequest request, .build() .toUriString(); + log.info("[OAuth2 SUCCESS] redirectUrl={}", redirectUrl); response.sendRedirect(redirectUrl); } - @SuppressWarnings("unchecked") - private String extractEmail(SocialType socialType, OAuth2User oAuth2User) { - Map attributes = oAuth2User.getAttributes(); + private void redirectFail(HttpServletResponse response, String errorCode) throws IOException { + String failUrl = UriComponentsBuilder.fromUriString(LOGIN_ERROR_REDIRECT_BASE) + .queryParam("error", errorCode) + .build() + .toUriString(); + log.warn("[OAuth2 FAIL] errorCode={}, failUrl={}", errorCode, failUrl); + response.sendRedirect(failUrl); + } + + private String extractEmail(SocialType socialType, Map attributes) { if (socialType == SocialType.GOOGLE) { Object email = attributes.get("email"); return email != null ? String.valueOf(email) : null; @@ -103,4 +130,40 @@ private String extractEmail(SocialType socialType, OAuth2User oAuth2User) { return null; } -} \ No newline at end of file + + private String extractSocialId(SocialType socialType, Map attributes) { + if (socialType == SocialType.GOOGLE) { + // 구글은 OIDC면 보통 sub가 안정적 식별자 (환경에 따라 없을 수 있음) + Object sub = attributes.get("sub"); + if (sub != null) return String.valueOf(sub); + + // fallback + Object id = attributes.get("id"); + return id != null ? String.valueOf(id) : null; + } + + if (socialType == SocialType.KAKAO) { + Object id = attributes.get("id"); + return id != null ? String.valueOf(id) : null; + } + + return null; + } + + @SuppressWarnings("unchecked") + private void logKakaoAccountStatus(Map attributes) { + Object kakaoAccountObj = attributes.get("kakao_account"); + if (!(kakaoAccountObj instanceof Map kakaoAccount)) { + log.warn("[KAKAO] kakao_account missing. attributes={}", attributes); + return; + } + + Object hasEmail = kakaoAccount.get("has_email"); + Object emailNeedsAgreement = kakaoAccount.get("email_needs_agreement"); + Object isEmailValid = kakaoAccount.get("is_email_valid"); + Object isEmailVerified = kakaoAccount.get("is_email_verified"); + + log.warn("[KAKAO] has_email={}, email_needs_agreement={}, is_email_valid={}, is_email_verified={}", + hasEmail, emailNeedsAgreement, isEmailValid, isEmailVerified); + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java index 3f66af06..1cf94a75 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java @@ -5,13 +5,15 @@ import com.eatsfine.eatsfine.domain.user.service.oauthService.CustomOAuth2MemberServiceImpl; import com.eatsfine.eatsfine.global.auth.CustomAccessDeniedHandler; import com.eatsfine.eatsfine.global.auth.CustomAuthenticationEntryPoint; + +import com.eatsfine.eatsfine.global.auth.HttpCookieOAuth2AuthorizationRequestRepository; import com.eatsfine.eatsfine.global.config.jwt.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpMethod; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @@ -24,8 +26,6 @@ import java.time.Duration; import java.util.List; -import static org.springframework.security.config.Customizer.withDefaults; - @Configuration @EnableWebSecurity @RequiredArgsConstructor @@ -34,11 +34,11 @@ public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CustomAuthenticationEntryPoint authenticationEntryPoint; private final CustomAccessDeniedHandler accessDeniedHandler; + private final CustomOAuth2MemberServiceImpl customOAuth2UserService; private final CustomOAuth2SuccessHandler customOAuth2SuccessHandler; private final CustomOAuth2FailureHandler customOAuth2FailureHandler; - @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -46,15 +46,17 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling(exceptions -> exceptions .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler) ) + .authorizeHttpRequests(auth -> auth - // preflight은 항상 허용 .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() - // 공개 리소스 / 인증 없이 + .requestMatchers("/oauth2/**", "/login/oauth2/**").permitAll() + .requestMatchers( "/api/auth/**", "/swagger-ui.html", @@ -64,18 +66,20 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/swagger-resources/**" ).permitAll() - .requestMatchers("/auth/**", "/login", "/signup").permitAll() - - // 그 외는 인증 필요 + .requestMatchers("/auth/**", "/login/**", "/signup").permitAll() .anyRequest().authenticated() ) + .oauth2Login(oauth2 -> oauth2 + // ✅ 핵심: 세션 대신 쿠키에 AuthorizationRequest 저장 + .authorizationEndpoint(authorization -> authorization + .authorizationRequestRepository(cookieAuthorizationRequestRepository()) + ) .userInfoEndpoint(userInfo -> userInfo .userService(customOAuth2UserService) ) .successHandler(customOAuth2SuccessHandler) .failureHandler(customOAuth2FailureHandler) - ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); @@ -84,12 +88,17 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { } @Bean - public CorsConfigurationSource corsConfigurationSource() { // cors 설정 + public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() { + return new HttpCookieOAuth2AuthorizationRequestRepository(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOriginPatterns(List.of("*")); // 운영 환경에서는 정확한 도메인만 명시 + config.setAllowedOriginPatterns(List.of("*")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); config.setAllowedHeaders(List.of("*")); - config.setExposedHeaders(List.of("Authorization", "Set-Cookie")); //쿠키, Authorization 헤더 노출 + config.setExposedHeaders(List.of("Authorization", "Set-Cookie")); config.setAllowCredentials(true); config.setMaxAge(Duration.ofHours(1)); @@ -98,9 +107,8 @@ public CorsConfigurationSource corsConfigurationSource() { // cors 설정 return source; } - @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } -} \ No newline at end of file +} From 8a2040157349218316dc557dbc918c428429c6a3 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 6 Feb 2026 04:07:41 +0900 Subject: [PATCH 53/83] =?UTF-8?q?[Fix]=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20null=20=EA=B0=92=20=EB=B0=98=ED=99=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=98=A4=EB=A5=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/user/converter/UserConverter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java index 1af78cc7..00ac3667 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java @@ -87,14 +87,15 @@ public static Term toUserTerm(UserRequestDto.JoinDto dto, User user) { 소셜 유저 생성 (최초 소셜 가입 등) -소셜 로그인에서 email/nickname/phoneNumber 등을 확보한 후 엔티티 생성에 사용 */ - public static User toSocialUser(String email, String nickName, String phoneNumber, String socialId, SocialType socialType) { + public static User toSocialUser(String email, String nickName, String profileImage, String socialId, SocialType socialType) { return User.builder() .email(email) .nickName(nickName) - .phoneNumber(phoneNumber) + .profileImage(profileImage) .socialId(socialId) .socialType(socialType) + .phoneNumber("") .role(ROLE_CUSTOMER) .build(); } From 8dacd1830dad6dd36f4bc785c6b2a245a7ade5f8 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 6 Feb 2026 04:10:18 +0900 Subject: [PATCH 54/83] =?UTF-8?q?[Fix]=20=EC=9A=B4=EC=98=81=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/handler/CustomOAuth2SuccessHandler.java | 4 ---- ...ttpCookieOAuth2AuthorizationRequestRepository.java | 11 +++-------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java index 5b1fd83b..efc299fe 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java @@ -57,10 +57,8 @@ public void onAuthenticationSuccess(HttpServletRequest request, OAuth2User oAuth2User = oauthToken.getPrincipal(); Map attrs = oAuth2User.getAttributes(); - // ✅ 디버깅 핵심 로그 log.info("[OAuth2 SUCCESS] provider={}, attrs={}", provider, attrs); - // 카카오의 경우 id도 추출해두면 원인 파악에 큰 도움 됨 String socialId = extractSocialId(socialType, attrs); log.info("[OAuth2 SUCCESS] provider={}, socialId={}", provider, socialId); @@ -68,7 +66,6 @@ public void onAuthenticationSuccess(HttpServletRequest request, log.info("[OAuth2 SUCCESS] provider={}, extractedEmail={}", provider, email); if (email == null || email.isBlank()) { - // 카카오 email 관련 상태값도 같이 찍어주면 디버깅 끝남 if (socialType == SocialType.KAKAO) { logKakaoAccountStatus(attrs); } @@ -133,7 +130,6 @@ private String extractEmail(SocialType socialType, Map attribute private String extractSocialId(SocialType socialType, Map attributes) { if (socialType == SocialType.GOOGLE) { - // 구글은 OIDC면 보통 sub가 안정적 식별자 (환경에 따라 없을 수 있음) Object sub = attributes.get("sub"); if (sub != null) return String.valueOf(sub); diff --git a/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java b/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java index b62c7f88..4481ce09 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java @@ -77,13 +77,8 @@ public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationReq cookie.setHttpOnly(true); cookie.setMaxAge(COOKIE_EXPIRE_SECONDS); - // ⭐ 환경에 따라 설정 - cookie.setSecure(false); // 로컬 HTTP: false, 운영 HTTPS: true - - // ⭐ 도메인 명시 (선택사항, 문제 있을 때만) - // cookie.setDomain("eatsfine.co.kr"); // 운영 환경 - // cookie.setDomain("localhost"); // 로컬 환경 - + // 환경에 따라 설정 + cookie.setSecure(true); // 로컬 HTTP: false, 운영 HTTPS: true response.addCookie(cookie); log.info("[SAVE] AuthorizationRequest 쿠키 저장 완료 - name={}, path={}, maxAge={}, secure={}, domain={}", @@ -108,7 +103,7 @@ private void removeAuthorizationRequestCookies(HttpServletResponse response) { cookie.setPath("/"); cookie.setHttpOnly(true); cookie.setMaxAge(0); - cookie.setSecure(false); // 저장할 때와 동일하게 + cookie.setSecure(true); // 저장할 때와 동일하게 cookie.setAttribute("SameSite", "Lax"); // 저장할 때와 동일하게 response.addCookie(cookie); From f5396fce51d1ccef937aab1b50804aba50e42160 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 6 Feb 2026 16:47:48 +0900 Subject: [PATCH 55/83] =?UTF-8?q?[Debug]=20refreshToken=20=EC=9E=AC?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthController.java | 8 +++- .../handler/CustomOAuth2SuccessHandler.java | 3 ++ .../authService/AuthTokenServiceImpl.java | 38 ++++++++++++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java index 91dd8da2..c8332407 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java @@ -2,11 +2,11 @@ import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; import com.eatsfine.eatsfine.domain.user.service.authService.AuthTokenService; -import com.eatsfine.eatsfine.domain.user.service.userService.UserService; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; @@ -14,6 +14,8 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; + +@Slf4j @RestController @RequiredArgsConstructor @RequestMapping("/api/auth") @@ -27,11 +29,15 @@ public ResponseEntity> reissue( @CookieValue(value = "refreshToken", required = false) String refreshToken, HttpServletResponse response ) { + log.info("[REISSUE API] 재발급 요청 받음. refreshToken={}", refreshToken); + AuthTokenService.ReissueResult result = authTokenService.reissue(refreshToken); ResponseCookie refreshCookie = authCookieProvider.refreshTokenCookie(result.refreshToken()); response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString()); + log.info("[REISSUE API] 재발급 성공. 새 쿠키 설정 완료"); + return ResponseEntity.ok( ApiResponse.onSuccess(new UserResponseDto.AccessTokenResponse(result.accessToken())) ); diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java index efc299fe..4d433c71 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java @@ -92,6 +92,9 @@ public void onAuthenticationSuccess(HttpServletRequest request, ResponseCookie refreshCookie = authCookieProvider.refreshTokenCookie(refreshToken); response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString()); + log.info("[OAuth2 SUCCESS] 새 refreshToken 쿠키 설정: {}", refreshToken); + log.info("[OAuth2 SUCCESS] 쿠키 설정 내용: {}", refreshCookie.toString()); + String redirectUrl = UriComponentsBuilder.fromUriString(CALLBACK_REDIRECT_BASE) .queryParam("accessToken", accessToken) .build() diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java index 008fd3dd..b2ac8eb8 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java @@ -6,6 +6,8 @@ import com.eatsfine.eatsfine.domain.user.status.AuthErrorStatus; import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,39 +16,73 @@ @Transactional public class AuthTokenServiceImpl implements AuthTokenService { + private static final Logger log = LoggerFactory.getLogger(AuthTokenServiceImpl.class); + private final JwtTokenProvider jwtTokenProvider; private final UserRepository userRepository; @Override public ReissueResult reissue(String refreshToken) { + log.info("[REISSUE] 재발급 요청 시작"); + log.info("[REISSUE] 요청 refreshToken={}", refreshToken); + if (refreshToken == null || refreshToken.isBlank()) { + log.error("[REISSUE] refreshToken이 null 또는 빈 값"); throw new AuthException(AuthErrorStatus.INVALID_TOKEN); } + log.info("[REISSUE] 토큰 검증 시작"); if (!jwtTokenProvider.validateToken(refreshToken)) { + log.error("[REISSUE] 토큰 검증 실패 - 만료되었거나 유효하지 않은 토큰"); throw new AuthException(AuthErrorStatus.INVALID_TOKEN); } + log.info("[REISSUE] 토큰 검증 성공"); String email = jwtTokenProvider.getEmailFromToken(refreshToken); + log.info("[REISSUE] 토큰에서 추출한 email={}", email); + if (email == null || email.isBlank()) { + log.error("[REISSUE] 토큰에서 email 추출 실패"); throw new AuthException(AuthErrorStatus.INVALID_TOKEN); } User user = userRepository.findByEmail(email) - .orElseThrow(() -> new AuthException(AuthErrorStatus.INVALID_TOKEN)); + .orElseThrow(() -> { + log.error("[REISSUE] 사용자를 찾을 수 없음. email={}", email); + return new AuthException(AuthErrorStatus.INVALID_TOKEN); + }); + + log.info("[REISSUE] 사용자 조회 성공. userId={}", user.getId()); + log.info("[REISSUE] DB에 저장된 refreshToken={}", user.getRefreshToken()); + log.info("[REISSUE] 요청받은 refreshToken={}", refreshToken); + log.info("[REISSUE] 토큰 일치 여부={}", + user.getRefreshToken() != null && user.getRefreshToken().equals(refreshToken)); // DB에 저장된 refreshToken과 쿠키 refreshToken이 같아야만 재발급 허용 if (user.getRefreshToken() == null || !user.getRefreshToken().equals(refreshToken)) { + log.error("[REISSUE] DB 토큰과 요청 토큰 불일치"); + log.error("[REISSUE] DB 토큰 null 여부={}", user.getRefreshToken() == null); + if (user.getRefreshToken() != null) { + log.error("[REISSUE] DB 토큰 길이={}, 요청 토큰 길이={}", + user.getRefreshToken().length(), refreshToken.length()); + } throw new AuthException(AuthErrorStatus.INVALID_TOKEN); } + log.info("[REISSUE] 토큰 일치 확인 완료. 새 토큰 발급 시작"); + // 새 토큰 발급 String newAccessToken = jwtTokenProvider.createAccessToken(email); String newRefreshToken = jwtTokenProvider.createRefreshToken(email); + log.info("[REISSUE] 새 accessToken 발급 완료"); + log.info("[REISSUE] 새 refreshToken 발급 완료. 새 토큰={}", newRefreshToken); + user.updateRefreshToken(newRefreshToken); + log.info("[REISSUE] DB 업데이트 완료. 재발급 성공"); + return new ReissueResult(newAccessToken, newRefreshToken); } } \ No newline at end of file From 619fa609ff42a796ad0a82a2ad9c8a19e9da451e Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 6 Feb 2026 16:48:17 +0900 Subject: [PATCH 56/83] =?UTF-8?q?[Fix]=20refreshToken=20=EC=BF=A0=ED=82=A4?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/user/controller/UserController.java | 7 ++++++- .../eatsfine/eatsfine/global/config/SecurityConfig.java | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java index d103cf93..27c3864e 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java @@ -113,7 +113,12 @@ public ResponseEntity> updateProfileImage( ) public ResponseEntity withdraw(HttpServletRequest request) { userService.withdraw(request); - return ResponseEntity.ok(ApiResponse.onSuccess("회원 탈퇴가 완료되었습니다.")); + + //회원탈퇴 시 refreshToken 쿠키도 삭제 + ResponseCookie clearCookie = authCookieProvider.clearRefreshTokenCookie(); + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, clearCookie.toString()) + .body(ApiResponse.onSuccess("회원 탈퇴가 완료되었습니다.")); } diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java index 1cf94a75..a980f7ac 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java @@ -71,7 +71,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { ) .oauth2Login(oauth2 -> oauth2 - // ✅ 핵심: 세션 대신 쿠키에 AuthorizationRequest 저장 .authorizationEndpoint(authorization -> authorization .authorizationRequestRepository(cookieAuthorizationRequestRepository()) ) From 82e8e739cae4599507242490a0a30cafb17ac52c Mon Sep 17 00:00:00 2001 From: SungMinju Date: Fri, 6 Feb 2026 21:41:51 +0900 Subject: [PATCH 57/83] =?UTF-8?q?[chore]=20=ED=94=84=EB=A1=A0=ED=8A=B8=20O?= =?UTF-8?q?Auth2=20redirect=20URL=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B8=ED=95=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/exception/handler/CustomOAuth2FailureHandler.java | 3 ++- .../user/exception/handler/CustomOAuth2SuccessHandler.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java index 9b11a92e..5e91bd5e 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2FailureHandler.java @@ -16,7 +16,8 @@ public class CustomOAuth2FailureHandler implements AuthenticationFailureHandler private static final Logger log = LoggerFactory.getLogger(CustomOAuth2FailureHandler.class); - private static final String ERROR_REDIRECT_BASE = "https://eatsfine.co.kr/login/error"; + private static final String ERROR_REDIRECT_BASE = "http://localhost:5173/login/error"; // 프론트 로컬 테스트 진행 + //"https://eatsfine.co.kr/login/error"; @Override public void onAuthenticationFailure(HttpServletRequest request, diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java index 4d433c71..c0a856e7 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java @@ -34,8 +34,8 @@ public class CustomOAuth2SuccessHandler implements AuthenticationSuccessHandler private final JwtTokenProvider jwtTokenProvider; private final AuthCookieProvider authCookieProvider; - private static final String CALLBACK_REDIRECT_BASE = "https://eatsfine.co.kr/oauth/callback"; - private static final String LOGIN_ERROR_REDIRECT_BASE = "https://eatsfine.co.kr/login/error"; + private static final String CALLBACK_REDIRECT_BASE = "http://localhost:5173/oauth/callback" ;//"https://eatsfine.co.kr/oauth/callback"; + private static final String LOGIN_ERROR_REDIRECT_BASE ="http://localhost:5173/login/error"; //"https://eatsfine.co.kr/login/error"; @Override public void onAuthenticationSuccess(HttpServletRequest request, From 418e7feb312a7e27d5f9f94d3ab46c1eab0252c9 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sat, 7 Feb 2026 02:59:20 +0900 Subject: [PATCH 58/83] =?UTF-8?q?merge=20=EC=98=A4=EB=A5=98=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 --- .coderabbit.yaml | 57 +++++++++++++ build.gradle | 3 +- .../controller/BusinessHoursController.java | 20 +++-- .../service/BusinessHoursCommandService.java | 6 +- .../BusinessHoursCommandServiceImpl.java | 15 +++- .../RealBusinessNumberValidator.java | 9 +++ .../menu/controller/MenuController.java | 41 +++++++--- .../menu/service/MenuCommandService.java | 12 +-- .../menu/service/MenuCommandServiceImpl.java | 37 ++++----- .../controller/PaymentWebhookController.java | 81 +++++++++++++++++++ .../dto/request/PaymentWebhookDTO.java | 26 ++++++ .../domain/payment/entity/Payment.java | 4 + .../payment/service/PaymentService.java | 77 ++++++++++++++++++ .../payment/service/TossPaymentService.java | 20 +++++ .../store/controller/StoreController.java | 21 +++-- .../domain/store/dto/StoreResDto.java | 4 +- .../eatsfine/domain/store/entity/Store.java | 2 +- .../store/service/StoreCommandService.java | 6 +- .../service/StoreCommandServiceImpl.java | 43 +++++++--- .../store/service/StoreQueryServiceImpl.java | 2 + .../domain/store/status/StoreErrorStatus.java | 2 +- .../store/validator/StoreValidator.java | 25 ++++++ .../controller/StoreTableController.java | 42 ++++++---- .../controller/StoreTableControllerDocs.java | 22 +++-- .../service/StoreTableCommandService.java | 12 +-- .../service/StoreTableCommandServiceImpl.java | 49 ++++++----- .../controller/TableLayoutController.java | 17 +++- .../controller/TableLayoutControllerDocs.java | 7 +- .../service/TableLayoutCommandService.java | 2 +- .../TableLayoutCommandServiceImpl.java | 12 ++- .../service/TableLayoutQueryService.java | 2 +- .../service/TableLayoutQueryServiceImpl.java | 8 +- .../controller/TableBlockController.java | 11 ++- .../controller/TableBlockControllerDocs.java | 6 +- .../service/TableBlockCommandService.java | 2 +- .../service/TableBlockCommandServiceImpl.java | 13 ++- .../controller/TableImageController.java | 19 +++-- .../service/TableImageCommandService.java | 4 +- .../service/TableImageCommandServiceImpl.java | 13 +-- .../user/controller/AuthController.java | 7 +- .../user/controller/UserController.java | 23 +++++- .../domain/user/converter/UserConverter.java | 20 +++-- .../user/dto/request/UserRequestDto.java | 23 +++++- .../user/dto/response/UserResponseDto.java | 9 ++- .../eatsfine/domain/user/entity/User.java | 25 ++++-- .../handler/CustomOAuth2SuccessHandler.java | 30 +++++-- .../service/authService/AuthTokenService.java | 4 +- .../authService/AuthTokenServiceImpl.java | 6 +- .../user/service/userService/UserService.java | 2 + .../service/userService/UserServiceImpl.java | 43 +++++++--- .../domain/user/status/AuthErrorStatus.java | 12 ++- .../domain/user/status/UserErrorStatus.java | 10 +-- .../domain/user/status/UserSuccessStatus.java | 40 +++++++++ .../global/annotation/CurrentUser.java | 14 ++++ .../handler/GeneralExceptionAdvice.java | 18 +++++ .../global/auth/UserDetailsServiceImpl.java | 10 ++- .../global/config/SecurityConfig.java | 20 +++-- .../config/jwt/JwtAuthenticationFilter.java | 33 ++++++-- .../global/config/jwt/JwtTokenProvider.java | 32 ++++++-- .../controller/HealthControllerTest.java | 36 +++++++++ src/test/resources/application.yml | 6 ++ 61 files changed, 939 insertions(+), 238 deletions(-) create mode 100644 .coderabbit.yaml create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentWebhookController.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/payment/dto/request/PaymentWebhookDTO.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/store/validator/StoreValidator.java create mode 100644 src/main/java/com/eatsfine/eatsfine/domain/user/status/UserSuccessStatus.java create mode 100644 src/main/java/com/eatsfine/eatsfine/global/annotation/CurrentUser.java diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..ce4274c3 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,57 @@ +language: ko-KR + +reviews: + profile: assertive + high_level_summary: true + review_status: true + request_changes_workflow: false + auto_review: + enabled: true + drafts: true + path_instructions: + - path: "src/**" + instructions: | + 다음 항목들을 꼼꼼하게 검토해줘. + + 1. **예외 처리** + - 예외가 적절히 처리되었는지 확인해줘. (try-catch, throws, ExceptionAdvice) + - 공통 예외 처리 모듈(예: GlobalHandler, ApiResponse 등)을 잘 활용했는지 확인. + - RuntimeException을 남발하지 않고, 의미 있는 커스텀 예외를 사용하는지 검토. + - 예외 메시지에 민감 정보(DB 정보, 사용자 정보 등)가 노출되지 않게 했는지 점검. + + 2. **코드 품질 & 가독성** + - 메소드/클래스가 단일 책임 원칙(SRP)에 맞게 구성되어 있는지. + - 중복 코드가 있는 경우, 유틸/공통 컴포넌트로 추출 가능한지. + - 의미 있는 변수명과 메소드명을 사용했는지. + - 매직 넘버, 하드코딩된 값이 존재하는지 점검. + + 3. **성능 및 효율성** + - 불필요한 DB 쿼리 호출, N+1 문제 가능성이 있는지 확인. + - Stream, loop, recursion 사용 시 시간복잡도/메모리 효율성을 고려했는지. + - 캐시 적용 가능성이 있거나, 과도한 연산이 반복되는 구간이 있는지. + + 4. **트랜잭션 관리** + - @Transactional이 필요한 메소드에 누락되지 않았는지. + - 읽기 전용 트랜잭션(readOnly = true)을 적절히 사용했는지. + - DB 일관성, 롤백 정책이 올바른지 검토. + + 5. **입력 검증 및 보안** + - @Valid, Bean Validation 등을 통한 입력값 검증이 되어 있는지. + - 비밀번호, 토큰 등 민감한 정보가 로깅되지 않는지. + + 6. **테스트** + - 단위 테스트가 충분히 작성되었는지, 핵심 로직의 검증이 누락되지 않았는지. + - Mocking을 통한 독립 테스트 구조를 유지했는지. + - 경계값 테스트, 예외 케이스 테스트가 포함되어 있는지. + + 8. **구조 및 설계** + - Controller, Service, Repository 등 계층 구조가 올바르게 나뉘어 있는지. + - DTO, Entity, Domain 객체 간 변환 로직이 명확하고 중복되지 않는지. + - Config 클래스에서 Bean 등록이 과도하거나 순환 참조 위험이 없는지. + path_filters: + - "!infra/**" + - "!build/**" + - "!.gradle/**" + - "!**/*.min.js" + - "!**/node_modules/**" + - "!config/**" diff --git a/build.gradle b/build.gradle index b4429de8..23961013 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,8 @@ dependencies { //security implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + // JWT implementation 'io.jsonwebtoken:jjwt-api:0.11.5' @@ -42,7 +44,6 @@ dependencies { // oauth implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - //swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.1' diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/controller/BusinessHoursController.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/controller/BusinessHoursController.java index 3c0aa8fb..4e16fbe0 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/controller/BusinessHoursController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/controller/BusinessHoursController.java @@ -4,12 +4,17 @@ import com.eatsfine.eatsfine.domain.businesshours.dto.BusinessHoursResDto; import com.eatsfine.eatsfine.domain.businesshours.service.BusinessHoursCommandService; import com.eatsfine.eatsfine.domain.businesshours.status.BusinessHoursSuccessStatus; +import com.eatsfine.eatsfine.global.annotation.CurrentUser; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.userdetails.User; import org.springframework.web.bind.annotation.*; +import java.security.Principal; + @Tag(name = "BusinessHours", description = "영업시간 관련 API") @RequestMapping("/api/v1") @RestController @@ -23,13 +28,15 @@ public class BusinessHoursController { description = "가게의 영업시간을 수정합니다." ) @PatchMapping("/stores/{storeId}/business-hours") + @PreAuthorize("hasRole('OWNER')") public ApiResponse updateBusinessHours( @PathVariable Long storeId, - @RequestBody BusinessHoursReqDto.UpdateBusinessHoursDto dto - ){ + @RequestBody BusinessHoursReqDto.UpdateBusinessHoursDto dto, + @CurrentUser User user + ){ return ApiResponse.of( BusinessHoursSuccessStatus._UPDATE_BUSINESS_HOURS_SUCCESS, - businessHoursCommandService.updateBusinessHours(storeId, dto) + businessHoursCommandService.updateBusinessHours(storeId, dto, user.getUsername()) ); } @@ -38,14 +45,15 @@ public ApiResponse updateBusinessHou description = "가게의 브레이크타임을 설정합니다." ) @PatchMapping("/stores/{storeId}/break-time") + @PreAuthorize("hasRole('OWNER')") public ApiResponse updateBreakTime( @PathVariable Long storeId, - @RequestBody BusinessHoursReqDto.UpdateBreakTimeDto dto + @RequestBody BusinessHoursReqDto.UpdateBreakTimeDto dto, + @CurrentUser User user ){ return ApiResponse.of( BusinessHoursSuccessStatus._UPDATE_BREAKTIME_SUCCESS, - businessHoursCommandService.updateBreakTime(storeId, dto) + businessHoursCommandService.updateBreakTime(storeId, dto, user.getUsername()) ); } - } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandService.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandService.java index e7526242..664748d8 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandService.java @@ -6,12 +6,14 @@ public interface BusinessHoursCommandService { BusinessHoursResDto.UpdateBusinessHoursDto updateBusinessHours( Long storeId, - BusinessHoursReqDto.UpdateBusinessHoursDto updateBusinessHoursDto + BusinessHoursReqDto.UpdateBusinessHoursDto updateBusinessHoursDto, + String email ); BusinessHoursResDto.UpdateBreakTimeDto updateBreakTime( Long storeId, - BusinessHoursReqDto.UpdateBreakTimeDto dto + BusinessHoursReqDto.UpdateBreakTimeDto dto, + String email ); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java index d5416dd8..f95cfa42 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businesshours/service/BusinessHoursCommandServiceImpl.java @@ -10,6 +10,7 @@ import com.eatsfine.eatsfine.domain.store.exception.StoreException; import com.eatsfine.eatsfine.domain.store.repository.StoreRepository; import com.eatsfine.eatsfine.domain.store.status.StoreErrorStatus; +import com.eatsfine.eatsfine.domain.store.validator.StoreValidator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,12 +21,17 @@ public class BusinessHoursCommandServiceImpl implements BusinessHoursCommandService { private final StoreRepository storeRepository; + private final StoreValidator storeValidator; @Override public BusinessHoursResDto.UpdateBusinessHoursDto updateBusinessHours( Long storeId, - BusinessHoursReqDto.UpdateBusinessHoursDto dto + BusinessHoursReqDto.UpdateBusinessHoursDto dto, + String email ) { + + storeValidator.validateStoreOwner(storeId, email); + // 영업시간 검증 BusinessHoursValidator.validateForUpdate(dto.businessHours()); @@ -47,10 +53,11 @@ public BusinessHoursResDto.UpdateBusinessHoursDto updateBusinessHours( @Override public BusinessHoursResDto.UpdateBreakTimeDto updateBreakTime( Long storeId, - BusinessHoursReqDto.UpdateBreakTimeDto dto + BusinessHoursReqDto.UpdateBreakTimeDto dto, + String email ) { - Store store = storeRepository.findById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); + + Store store = storeValidator.validateStoreOwner(storeId, email); for(BusinessHours bh : store.getBusinessHours()) { if(bh.isClosed()) continue; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/businessnumber/validator/RealBusinessNumberValidator.java b/src/main/java/com/eatsfine/eatsfine/domain/businessnumber/validator/RealBusinessNumberValidator.java index 68cee2a1..0d5f7f80 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/businessnumber/validator/RealBusinessNumberValidator.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/businessnumber/validator/RealBusinessNumberValidator.java @@ -73,7 +73,16 @@ public void validate(String businessNumber, String startDate, String representat throw new BusinessNumberException(BusinessNumberErrorStatus._INVALID_BUSINESS_NUMBER); } + log.info("[BusinessNumber API] 인증 통과 - 번호: {}", maskBusinessNumber(businessNumber)); + }; + private String maskBusinessNumber(String businessNumber) { + if (businessNumber == null || businessNumber.length() < 6) { + return "***"; + } + return businessNumber.substring(0, 3) + "****" + businessNumber.substring(businessNumber.length() - 3); + } + } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java index cfc3a737..9a1c4042 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/controller/MenuController.java @@ -5,12 +5,15 @@ import com.eatsfine.eatsfine.domain.menu.service.MenuCommandService; import com.eatsfine.eatsfine.domain.menu.service.MenuQueryService; import com.eatsfine.eatsfine.domain.menu.status.MenuSuccessStatus; +import com.eatsfine.eatsfine.global.annotation.CurrentUser; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; 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.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.userdetails.User; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -25,58 +28,70 @@ public class MenuController { @Operation(summary = "메뉴 이미지 선 업로드 API", description = "메뉴 등록 전에 이미지를 먼저 업로드하고 KEY를 반환합니다.") @PostMapping(value = "/stores/{storeId}/menus/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PreAuthorize("hasRole('OWNER')") public ApiResponse uploadImage( @PathVariable Long storeId, - @RequestPart("image") MultipartFile file - ){ - return ApiResponse.of(MenuSuccessStatus._MENU_IMAGE_UPLOAD_SUCCESS, menuCommandService.uploadImage(storeId, file)); + @RequestPart("image") MultipartFile file, + @CurrentUser User user + ){ + return ApiResponse.of(MenuSuccessStatus._MENU_IMAGE_UPLOAD_SUCCESS, menuCommandService.uploadImage(storeId, file, user.getUsername())); } @Operation(summary = "메뉴 등록 API", description = "가게의 메뉴들을 등록합니다.") @PostMapping("/stores/{storeId}/menus") + @PreAuthorize("hasRole('OWNER')") public ApiResponse createMenus( @PathVariable Long storeId, - @RequestBody @Valid MenuReqDto.MenuCreateDto dto + @RequestBody @Valid MenuReqDto.MenuCreateDto dto, + @CurrentUser User user ) { - return ApiResponse.of(MenuSuccessStatus._MENU_CREATE_SUCCESS, menuCommandService.createMenus(storeId, dto)); + return ApiResponse.of(MenuSuccessStatus._MENU_CREATE_SUCCESS, menuCommandService.createMenus(storeId, dto, user.getUsername())); } @Operation(summary = "메뉴 삭제 API", description = "가게의 메뉴들을 삭제합니다.") @DeleteMapping("/stores/{storeId}/menus") + @PreAuthorize("hasRole('OWNER')") public ApiResponse deleteMenus( @PathVariable Long storeId, - @RequestBody @Valid MenuReqDto.MenuDeleteDto dto + @RequestBody @Valid MenuReqDto.MenuDeleteDto dto, + @CurrentUser User user ) { - return ApiResponse.of(MenuSuccessStatus._MENU_DELETE_SUCCESS, menuCommandService.deleteMenus(storeId, dto)); + return ApiResponse.of(MenuSuccessStatus._MENU_DELETE_SUCCESS, menuCommandService.deleteMenus(storeId, dto, user.getUsername())); } @Operation(summary = "메뉴 수정 API", description = "가게의 메뉴를 수정합니다.") @PatchMapping("/stores/{storeId}/menus/{menuId}") + @PreAuthorize("hasRole('OWNER')") public ApiResponse updateMenu( @PathVariable Long storeId, @PathVariable Long menuId, - @RequestBody @Valid MenuReqDto.MenuUpdateDto dto + @RequestBody @Valid MenuReqDto.MenuUpdateDto dto, + @CurrentUser User user ) { - return ApiResponse.of(MenuSuccessStatus._MENU_UPDATE_SUCCESS, menuCommandService.updateMenu(storeId, menuId, dto)); + return ApiResponse.of(MenuSuccessStatus._MENU_UPDATE_SUCCESS, menuCommandService.updateMenu(storeId, menuId, dto, user.getUsername())); } @Operation(summary = "품절 여부 변경 API", description = "메뉴의 품절 여부를 변경합니다.") @PatchMapping("/stores/{storeId}/menus/{menuId}/sold-out") + @PreAuthorize("hasRole('OWNER')") public ApiResponse updateSoldOutStatus( @PathVariable Long storeId, @PathVariable Long menuId, - @RequestBody @Valid MenuReqDto.SoldOutUpdateDto dto + @RequestBody @Valid MenuReqDto.SoldOutUpdateDto dto, + @CurrentUser User user ){ - return ApiResponse.of(MenuSuccessStatus._SOLD_OUT_UPDATE_SUCCESS, menuCommandService.updateSoldOutStatus(storeId, menuId, dto.isSoldOut())); + return ApiResponse.of(MenuSuccessStatus._SOLD_OUT_UPDATE_SUCCESS, menuCommandService.updateSoldOutStatus(storeId, menuId, dto.isSoldOut(), user.getUsername())); } @Operation(summary = "등록된 메뉴 이미지 삭제 API", description = "이미 등록된 메뉴의 이미지를 삭제합니다.") @DeleteMapping("/stores/{storeId}/menus/{menuId}/image") + @PreAuthorize("hasRole('OWNER')") public ApiResponse deleteMenuImage( @PathVariable Long storeId, - @PathVariable Long menuId + @PathVariable Long menuId, + @CurrentUser User user ) { - return ApiResponse.of(MenuSuccessStatus._MENU_IMAGE_DELETE_SUCCESS, menuCommandService.deleteMenuImage(storeId, menuId)); + return ApiResponse.of(MenuSuccessStatus._MENU_IMAGE_DELETE_SUCCESS, menuCommandService.deleteMenuImage(storeId, menuId, user.getUsername())); } @Operation(summary = "메뉴 조회 API", description = "가게의 메뉴들을 조회합니다.") diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java index 862e407b..1ef1ed93 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandService.java @@ -5,11 +5,11 @@ import org.springframework.web.multipart.MultipartFile; public interface MenuCommandService { - MenuResDto.ImageUploadDto uploadImage(Long storeId, MultipartFile file); - MenuResDto.ImageDeleteDto deleteMenuImage(Long storeId, Long menuId); - MenuResDto.MenuCreateDto createMenus(Long storeId, MenuReqDto.MenuCreateDto menuCreateDto); - MenuResDto.MenuDeleteDto deleteMenus(Long storeId, MenuReqDto.MenuDeleteDto menuDeleteDto); - MenuResDto.MenuUpdateDto updateMenu(Long storeId, Long menuId, MenuReqDto.MenuUpdateDto menuUpdateDto); - MenuResDto.SoldOutUpdateDto updateSoldOutStatus(Long storeId, Long menuId, boolean isSoldOut); + MenuResDto.ImageUploadDto uploadImage(Long storeId, MultipartFile file, String email); + MenuResDto.ImageDeleteDto deleteMenuImage(Long storeId, Long menuId, String email); + MenuResDto.MenuCreateDto createMenus(Long storeId, MenuReqDto.MenuCreateDto menuCreateDto, String email); + MenuResDto.MenuDeleteDto deleteMenus(Long storeId, MenuReqDto.MenuDeleteDto menuDeleteDto, String email); + MenuResDto.MenuUpdateDto updateMenu(Long storeId, Long menuId, MenuReqDto.MenuUpdateDto menuUpdateDto, String email); + MenuResDto.SoldOutUpdateDto updateSoldOutStatus(Long storeId, Long menuId, boolean isSoldOut, String email); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java index 9685c386..55699911 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/menu/service/MenuCommandServiceImpl.java @@ -13,6 +13,7 @@ import com.eatsfine.eatsfine.domain.store.exception.StoreException; import com.eatsfine.eatsfine.domain.store.repository.StoreRepository; import com.eatsfine.eatsfine.domain.store.status.StoreErrorStatus; +import com.eatsfine.eatsfine.domain.store.validator.StoreValidator; import com.eatsfine.eatsfine.global.s3.S3Service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -36,10 +37,11 @@ public class MenuCommandServiceImpl implements MenuCommandService { private final S3Service s3Service; private final StoreRepository storeRepository; private final MenuRepository menuRepository; + private final StoreValidator storeValidator; @Override - public MenuResDto.MenuCreateDto createMenus(Long storeId, MenuReqDto.MenuCreateDto dto) { - Store store = findAndVerifyStore(storeId); + public MenuResDto.MenuCreateDto createMenus(Long storeId, MenuReqDto.MenuCreateDto dto, String email) { + Store store = storeValidator.validateStoreOwner(storeId, email); List menus = dto.menus().stream() .map(menuDto -> { @@ -96,8 +98,8 @@ public void afterCommit(){ } @Override - public MenuResDto.MenuDeleteDto deleteMenus(Long storeId, MenuReqDto.MenuDeleteDto dto) { - Store store = findAndVerifyStore(storeId); + public MenuResDto.MenuDeleteDto deleteMenus(Long storeId, MenuReqDto.MenuDeleteDto dto, String email) { + Store store = storeValidator.validateStoreOwner(storeId, email); List menuIds = dto.menuIds(); List menusToDelete = menuRepository.findAllById(dto.menuIds()); @@ -132,10 +134,8 @@ public void afterCommit() { } @Override - public MenuResDto.MenuUpdateDto updateMenu(Long storeId, Long menuId, MenuReqDto.MenuUpdateDto dto) { - Store store = findAndVerifyStore(storeId); - - // TODO: [보안] Spring Security 병합 후, 현재 로그인한 사용자가 이 가게의 주인인지 확인하는 로직 추가 + public MenuResDto.MenuUpdateDto updateMenu(Long storeId, Long menuId, MenuReqDto.MenuUpdateDto dto, String email) { + Store store = storeValidator.validateStoreOwner(storeId, email); Menu menu = menuRepository.findById(menuId) .orElseThrow(() -> new MenuException(MenuErrorStatus._MENU_NOT_FOUND)); @@ -198,8 +198,8 @@ public void afterCommit() { } @Override - public MenuResDto.SoldOutUpdateDto updateSoldOutStatus(Long storeId, Long menuId, boolean isSoldOut) { - findAndVerifyStore(storeId); + public MenuResDto.SoldOutUpdateDto updateSoldOutStatus(Long storeId, Long menuId, boolean isSoldOut, String email) { + storeValidator.validateStoreOwner(storeId, email); Menu menu = menuRepository.findById(menuId) .orElseThrow(() -> new MenuException(MenuErrorStatus._MENU_NOT_FOUND)); @@ -218,8 +218,8 @@ public MenuResDto.SoldOutUpdateDto updateSoldOutStatus(Long storeId, Long menuId } @Override - public MenuResDto.ImageUploadDto uploadImage(Long storeId, MultipartFile file) { - Store store = findAndVerifyStore(storeId); + public MenuResDto.ImageUploadDto uploadImage(Long storeId, MultipartFile file, String email) { + storeValidator.validateStoreOwner(storeId, email); if(file.isEmpty()) { throw new ImageException(ImageErrorStatus.EMPTY_FILE); @@ -233,8 +233,8 @@ public MenuResDto.ImageUploadDto uploadImage(Long storeId, MultipartFile file) { } @Override - public MenuResDto.ImageDeleteDto deleteMenuImage(Long storeId, Long menuId) { - findAndVerifyStore(storeId); + public MenuResDto.ImageDeleteDto deleteMenuImage(Long storeId, Long menuId, String email) { + storeValidator.validateStoreOwner(storeId, email); Menu menu = menuRepository.findById(menuId) .orElseThrow(() -> new MenuException(MenuErrorStatus._MENU_NOT_FOUND)); @@ -262,17 +262,10 @@ public void afterCommit() { return MenuConverter.toImageDeleteDto(imageKey); // 삭제된 이미지의 키를 반환 } - private Store findAndVerifyStore(Long storeId) { - Store store = storeRepository.findById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); - // TODO: [보안] Spring Security 병합 후, 현재 로그인한 사용자가 이 가게의 주인인지 확인하는 로직 추가 - return store; - } - private void verifyMenuBelongsToStore(Menu menu, Long storeId) { if (!menu.getStore().getId().equals(storeId)) { // 다른 가게의 메뉴를 조작하려는 시도 방지 - throw new StoreException(StoreErrorStatus._STORE_NOT_OWNER); + throw new StoreException(StoreErrorStatus._NOT_STORE_OWNER); } } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentWebhookController.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentWebhookController.java new file mode 100644 index 00000000..4aec3c4d --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/controller/PaymentWebhookController.java @@ -0,0 +1,81 @@ +package com.eatsfine.eatsfine.domain.payment.controller; + +import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentWebhookDTO; +import com.eatsfine.eatsfine.domain.payment.exception.PaymentException; +import com.eatsfine.eatsfine.domain.payment.service.PaymentService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.web.bind.annotation.RequestHeader; +import jakarta.validation.Validator; +import jakarta.validation.ConstraintViolation; +import java.util.Set; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/payments/webhook") +@Tag(name = "Payment Webhook Controller", description = "Toss Payments 웹훅 수신 전용 컨트롤러") +public class PaymentWebhookController { + + private final PaymentService paymentService; + private final com.eatsfine.eatsfine.domain.payment.service.TossPaymentService tossPaymentService; + private final ObjectMapper objectMapper; + private final Validator validator; + + @Operation(summary = "Toss Payments 웹훅 수신", description = "Toss Payments 서버로부터 결제/취소 결과(PaymentKey, Status 등)를 수신하여 서버 상태를 동기화합니다.") + @PostMapping + public ResponseEntity handleWebhook( + @RequestBody String jsonBody, + @RequestHeader("tosspayments-webhook-signature") String signature, + @RequestHeader("tosspayments-webhook-transmission-time") String timestamp) throws JsonProcessingException { + + try { + tossPaymentService.verifyWebhookSignature(jsonBody, signature, timestamp); + } catch (Exception e) { + log.error("Webhook signature verification failed", e); + return ResponseEntity.status(401).body("Invalid Signature"); + } + + PaymentWebhookDTO dto = objectMapper.readValue(jsonBody, PaymentWebhookDTO.class); + + if (hasValidationErrors(dto)) { + return ResponseEntity.badRequest().body("Validation failed"); + } + + log.info("Webhook received: orderId={}, status={}", dto.data().orderId(), dto.data().status()); + + try { + paymentService.processWebhook(dto); + } catch (PaymentException e) { + log.error("Webhook processing failed (Business Logic): {}", e.getMessage()); + return ResponseEntity.ok("Ignored: " + e.getMessage()); + } catch (Exception e) { + log.error("Webhook processing failed (System Error)", e); + return ResponseEntity.internalServerError().body("Internal Server Error"); + } + + return ResponseEntity.ok("Received"); + } + + private boolean hasValidationErrors(PaymentWebhookDTO dto) { + Set> violations = validator.validate(dto); + if (!violations.isEmpty()) { + StringBuilder sb = new StringBuilder(); + for (ConstraintViolation violation : violations) { + sb.append(violation.getPropertyPath()).append(" ").append(violation.getMessage()).append("; "); + } + log.error("Webhook validation failed: {}", sb.toString()); + return true; + } + return false; + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/dto/request/PaymentWebhookDTO.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/dto/request/PaymentWebhookDTO.java new file mode 100644 index 00000000..b9077367 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/dto/request/PaymentWebhookDTO.java @@ -0,0 +1,26 @@ +package com.eatsfine.eatsfine.domain.payment.dto.request; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record PaymentWebhookDTO( + @NotBlank String eventType, + @Valid @NotNull PaymentData data) { + @JsonIgnoreProperties(ignoreUnknown = true) + public record PaymentData( + @NotBlank String paymentKey, + @NotBlank String orderId, + @NotBlank String status, + BigDecimal totalAmount, + EasyPay easyPay) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public record EasyPay( + String provider) { + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/entity/Payment.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/entity/Payment.java index d305a192..1bd5a61e 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/entity/Payment.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/entity/Payment.java @@ -25,6 +25,10 @@ public class Payment extends BaseEntity { @Column(name = "payment_id") private Long id; + // 낙관적 락을 위한 버전 필드 + @Version + private Long version; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "booking_id", nullable = false) private Booking booking; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java index 6cec0114..9ca43c5a 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/service/PaymentService.java @@ -2,6 +2,7 @@ import com.eatsfine.eatsfine.domain.booking.entity.Booking; import com.eatsfine.eatsfine.domain.booking.repository.BookingRepository; +import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentWebhookDTO; import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentConfirmDTO; import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentRequestDTO; import com.eatsfine.eatsfine.domain.payment.dto.response.PaymentResponseDTO; @@ -220,4 +221,80 @@ public PaymentResponseDTO.PaymentDetailResultDTO getPaymentDetail(Long paymentId null // 환불 상세 정보는 현재 null 처리 ); } + + @Transactional + public void processWebhook(PaymentWebhookDTO dto) { + // 이벤트 타입 검증 + if (!"PAYMENT_STATUS_CHANGED".equals(dto.eventType())) { + log.info("Webhook skipped: Unhandled event type {}", dto.eventType()); + return; + } + + PaymentWebhookDTO.PaymentData data = dto.data(); + + Payment payment = paymentRepository.findByOrderId(data.orderId()) + .orElseThrow(() -> new PaymentException(PaymentErrorStatus._PAYMENT_NOT_FOUND)); + + PaymentStatus targetStatus = null; + if ("DONE".equals(data.status())) { + targetStatus = PaymentStatus.COMPLETED; + } else if ("CANCELED".equals(data.status())) { + targetStatus = PaymentStatus.REFUNDED; + } + + if (targetStatus == null) { + log.info("Webhook skipped: Unknown or unhandled status {}", data.status()); + return; + } + + if (payment.getPaymentStatus() == targetStatus) { + log.info("Webhook skipped: Payment {} already in status {}", data.orderId(), targetStatus); + return; + } + + // 상태 전환 유효성 검사 + // COMPLETED 완료 처리는 오직 PENDING 상태에서만 가능 + if (targetStatus == PaymentStatus.COMPLETED && payment.getPaymentStatus() != PaymentStatus.PENDING) { + log.warn("Webhook skipped: Invalid state transition from {} to {} for OrderID {}", + payment.getPaymentStatus(), targetStatus, data.orderId()); + return; + } + if (targetStatus == PaymentStatus.REFUNDED && payment.getPaymentStatus() != PaymentStatus.COMPLETED) { + log.warn("Webhook skipped: Invalid state transition from {} to {} for OrderID {}", + payment.getPaymentStatus(), targetStatus, data.orderId()); + return; + } + + if (targetStatus == PaymentStatus.COMPLETED) { + // 금액 검증 + if (data.totalAmount() == null || payment.getAmount().compareTo(data.totalAmount()) != 0) { + log.error("Webhook amount verification failed for OrderID: {}. Expected: {}, Received: {}", + data.orderId(), payment.getAmount(), data.totalAmount()); + payment.failPayment(); + return; + } + + // Provider 파싱 + PaymentProvider provider = null; + if (data.easyPay() != null) { + String providerCode = data.easyPay().provider(); + if ("토스페이".equals(providerCode)) { + provider = PaymentProvider.TOSS; + } else if ("카카오페이".equals(providerCode)) { + provider = PaymentProvider.KAKAOPAY; + } + } + + payment.completePayment( + LocalDateTime.now(), + PaymentMethod.SIMPLE_PAYMENT, + data.paymentKey(), + provider, + null); + log.info("Webhook processed: Payment {} status updated to COMPLETED", data.orderId()); + } else if (targetStatus == PaymentStatus.REFUNDED) { + payment.cancelPayment(); + log.info("Webhook processed: Payment {} status updated to REFUNDED", data.orderId()); + } + } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/payment/service/TossPaymentService.java b/src/main/java/com/eatsfine/eatsfine/domain/payment/service/TossPaymentService.java index 4a434f8c..de69dedb 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/payment/service/TossPaymentService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/payment/service/TossPaymentService.java @@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; +import org.springframework.beans.factory.annotation.Value; @Slf4j @Service @@ -16,6 +17,9 @@ public class TossPaymentService { private final RestClient tossPaymentClient; + @Value("${payment.toss.widget-secret-key}") + private String widgetSecretKey; + public TossPaymentService(@Qualifier("tossPaymentClient") RestClient tossPaymentClient) { this.tossPaymentClient = tossPaymentClient; } @@ -45,4 +49,20 @@ public TossPaymentResponse cancel(String paymentKey, PaymentRequestDTO.CancelPay throw new GeneralException(ErrorStatus._INTERNAL_SERVER_ERROR); } } + + public void verifyWebhookSignature(String jsonBody, String signature, String timestamp) throws Exception { + String payload = timestamp + "." + jsonBody; + String calculatedSignature = hmacSha256(payload, widgetSecretKey); + + if (!signature.contains("v1:" + calculatedSignature)) { + throw new SecurityException("Signature verification failed"); + } + } + + private String hmacSha256(String data, String key) throws Exception { + javax.crypto.Mac sha256_HMAC = javax.crypto.Mac.getInstance("HmacSHA256"); + javax.crypto.spec.SecretKeySpec secret_key = new javax.crypto.spec.SecretKeySpec(key.getBytes(java.nio.charset.StandardCharsets.UTF_8), "HmacSHA256"); + sha256_HMAC.init(secret_key); + return java.util.Base64.getEncoder().encodeToString(sha256_HMAC.doFinal(data.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/controller/StoreController.java b/src/main/java/com/eatsfine/eatsfine/domain/store/controller/StoreController.java index aab3686a..dbbcc4a6 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/controller/StoreController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/controller/StoreController.java @@ -6,6 +6,7 @@ import com.eatsfine.eatsfine.domain.store.service.StoreCommandService; import com.eatsfine.eatsfine.domain.store.service.StoreQueryService; import com.eatsfine.eatsfine.domain.store.status.StoreSuccessStatus; +import com.eatsfine.eatsfine.global.annotation.CurrentUser; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -13,6 +14,8 @@ import lombok.RequiredArgsConstructor; import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.userdetails.User; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -30,10 +33,12 @@ public class StoreController { description = "사장 회원이 새로운 식당을 등록합니다" ) @PostMapping("/stores") + @PreAuthorize("hasRole('OWNER')") public ApiResponse createStore( - @Valid @RequestBody StoreReqDto.StoreCreateDto dto + @Valid @RequestBody StoreReqDto.StoreCreateDto dto, + @CurrentUser User user ) { - return ApiResponse.of(StoreSuccessStatus._STORE_CREATED, storeCommandService.createStore(dto)); + return ApiResponse.of(StoreSuccessStatus._STORE_CREATED, storeCommandService.createStore(dto, user.getUsername())); } @Operation( @@ -65,11 +70,13 @@ public ApiResponse getStoreDetail(@PathVariable Long "영업시간, 브레이크타임, 이미지는 별도 엔티티/컬렉션이므로 개별 API로 분리" ) @PatchMapping("/stores/{storeId}") + @PreAuthorize("hasRole('OWNER')") public ApiResponse updateStoreBasicInfo( @PathVariable Long storeId, - @Valid @RequestBody StoreReqDto.StoreUpdateDto dto + @Valid @RequestBody StoreReqDto.StoreUpdateDto dto, + @CurrentUser User user ) { - return ApiResponse.of(StoreSuccessStatus._STORE_UPDATE_SUCCESS, storeCommandService.updateBasicInfo(storeId, dto)); + return ApiResponse.of(StoreSuccessStatus._STORE_UPDATE_SUCCESS, storeCommandService.updateBasicInfo(storeId, dto, user.getUsername())); } @@ -81,11 +88,13 @@ public ApiResponse updateStoreBasicInfo( value = "/stores/{storeId}/main-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE ) + @PreAuthorize("hasRole('OWNER')") public ApiResponse uploadMainImage( @RequestPart("mainImage")MultipartFile mainImage, - @PathVariable Long storeId + @PathVariable Long storeId, + @CurrentUser User user ){ - return ApiResponse.of(StoreSuccessStatus._STORE_MAIN_IMAGE_UPLOAD_SUCCESS, storeCommandService.uploadMainImage(storeId, mainImage)); + return ApiResponse.of(StoreSuccessStatus._STORE_MAIN_IMAGE_UPLOAD_SUCCESS, storeCommandService.uploadMainImage(storeId, mainImage, user.getUsername())); } @Operation( diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/dto/StoreResDto.java b/src/main/java/com/eatsfine/eatsfine/domain/store/dto/StoreResDto.java index 8431ff63..8e21cae6 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/dto/StoreResDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/dto/StoreResDto.java @@ -34,7 +34,9 @@ public record StoreSearchDto( public record PaginationDto( int currentPage, int totalPages, - long totalCount + long totalCount, + boolean isFirst, + boolean isLast ){} @Builder diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java index 61d4b728..97c881e7 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/entity/Store.java @@ -40,7 +40,7 @@ public class Store extends BaseEntity { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "owner_id") // 임시 nullable 허용 (User 도메인 머지 후 owner 처리 예정) + @JoinColumn(name = "owner_id", nullable = false) private User owner; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreCommandService.java b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreCommandService.java index 33d39189..6d1cae27 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreCommandService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreCommandService.java @@ -5,7 +5,7 @@ import org.springframework.web.multipart.MultipartFile; public interface StoreCommandService { - StoreResDto.StoreCreateDto createStore(StoreReqDto.StoreCreateDto storeCreateDto); - StoreResDto.StoreUpdateDto updateBasicInfo(Long storeId, StoreReqDto.StoreUpdateDto storeUpdateDto); - StoreResDto.UploadMainImageDto uploadMainImage(Long storeId, MultipartFile file); + StoreResDto.StoreCreateDto createStore(StoreReqDto.StoreCreateDto storeCreateDto, String email); + StoreResDto.StoreUpdateDto updateBasicInfo(Long storeId, StoreReqDto.StoreUpdateDto storeUpdateDto, String email); + StoreResDto.UploadMainImageDto uploadMainImage(Long storeId, MultipartFile file, String email); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreCommandServiceImpl.java index e9054cc4..30d9256f 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreCommandServiceImpl.java @@ -15,7 +15,11 @@ import com.eatsfine.eatsfine.domain.store.entity.Store; import com.eatsfine.eatsfine.domain.store.exception.StoreException; import com.eatsfine.eatsfine.domain.store.repository.StoreRepository; -import com.eatsfine.eatsfine.domain.store.status.StoreErrorStatus; +import com.eatsfine.eatsfine.domain.store.validator.StoreValidator; +import com.eatsfine.eatsfine.domain.user.entity.User; +import com.eatsfine.eatsfine.domain.user.exception.UserException; +import com.eatsfine.eatsfine.domain.user.repository.UserRepository; +import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -24,6 +28,8 @@ import java.util.ArrayList; import java.util.List; import com.eatsfine.eatsfine.global.s3.S3Service; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.web.multipart.MultipartFile; @Service @@ -36,13 +42,21 @@ public class StoreCommandServiceImpl implements StoreCommandService { private final RegionRepository regionRepository; private final S3Service s3Service; private final BusinessNumberValidator businessNumberValidator; + private final StoreValidator storeValidator; + private final UserRepository userRepository; // 가게 등록 @Override - public StoreResDto.StoreCreateDto createStore(StoreReqDto.StoreCreateDto dto) { + public StoreResDto.StoreCreateDto createStore(StoreReqDto.StoreCreateDto dto, String email) { + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UserException(UserErrorStatus.MEMBER_NOT_FOUND)); + + businessNumberValidator.validate( + dto.businessNumberDto().businessNumber(), + dto.businessNumberDto().startDate(), + user.getName()); - // TODO: 추후 Security Context 연동 시, 로그인된 사용자의 이름을 가져오도록 수정 예정 - businessNumberValidator.validate(dto.businessNumberDto().businessNumber(), dto.businessNumberDto().startDate(), "홍길동"); log.info("사업자 번호 검증 성공: {}", dto.businessNumberDto().businessNumber()); @@ -55,7 +69,7 @@ public StoreResDto.StoreCreateDto createStore(StoreReqDto.StoreCreateDto dto) { BusinessHoursValidator.validateForCreate(dto.businessHours()); Store store = Store.builder() - .owner(null) // User 도메인 머지 후 owner 처리 예정 + .owner(user) .storeName(dto.storeName()) .businessNumber(dto.businessNumberDto().businessNumber()) .description(dto.description()) @@ -82,9 +96,8 @@ public StoreResDto.StoreCreateDto createStore(StoreReqDto.StoreCreateDto dto) { // 가게 기본 정보 수정 (필드) @Override - public StoreResDto.StoreUpdateDto updateBasicInfo(Long storeId, StoreReqDto.StoreUpdateDto dto) { - Store store = storeRepository.findById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); + public StoreResDto.StoreUpdateDto updateBasicInfo(Long storeId, StoreReqDto.StoreUpdateDto dto, String email) { + Store store = storeValidator.validateStoreOwner(storeId, email); store.updateBasicInfo(dto); List updatedFields = extractUpdatedFields(dto); @@ -107,17 +120,21 @@ public List extractUpdatedFields(StoreReqDto.StoreUpdateDto dto) { } // 가게 메인 이미지 등록 @Override - public StoreResDto.UploadMainImageDto uploadMainImage(Long storeId, MultipartFile file) { - Store store = storeRepository.findById(storeId).orElseThrow( - () -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND) - ); + public StoreResDto.UploadMainImageDto uploadMainImage(Long storeId, MultipartFile file, String email) { + Store store = storeValidator.validateStoreOwner(storeId, email); if(file.isEmpty()) { throw new ImageException(ImageErrorStatus.EMPTY_FILE); } if(store.getMainImageKey() != null) { - s3Service.deleteByKey(store.getMainImageKey()); + String oldKey = store.getMainImageKey(); + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + s3Service.deleteByKey(oldKey); + } + }); } String key = s3Service.upload(file, "stores/" + storeId + "/main"); diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java index ea3406f4..0239e538 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/service/StoreQueryServiceImpl.java @@ -61,6 +61,8 @@ public StoreResDto.StoreSearchResDto search( .currentPage(page) .totalPages(resultPage.getTotalPages()) .totalCount(resultPage.getTotalElements()) + .isFirst(resultPage.isFirst()) + .isLast(resultPage.isLast()) .build() ) .build(); diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/status/StoreErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/store/status/StoreErrorStatus.java index 0e0f4557..19205d2f 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/store/status/StoreErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/status/StoreErrorStatus.java @@ -15,7 +15,7 @@ public enum StoreErrorStatus implements BaseErrorCode { _STORE_DETAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE_DETAIL404", "가게 상세 정보를 찾을 수 없습니다."), _STORE_NOT_OPEN_ON_DAY(HttpStatus.NOT_FOUND,"STORE4041" , "해당 영업시간 정보를 찾을 수 없습니다."), - _STORE_NOT_OWNER(HttpStatus.FORBIDDEN, "STORE403", "해당 가게의 주인이 아닙니다."), + _NOT_STORE_OWNER(HttpStatus.FORBIDDEN, "STORE403", "해당 가게의 주인이 아닙니다."), ; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/store/validator/StoreValidator.java b/src/main/java/com/eatsfine/eatsfine/domain/store/validator/StoreValidator.java new file mode 100644 index 00000000..5d0e3aee --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/store/validator/StoreValidator.java @@ -0,0 +1,25 @@ +package com.eatsfine.eatsfine.domain.store.validator; + +import com.eatsfine.eatsfine.domain.store.entity.Store; +import com.eatsfine.eatsfine.domain.store.exception.StoreException; +import com.eatsfine.eatsfine.domain.store.repository.StoreRepository; +import com.eatsfine.eatsfine.domain.store.status.StoreErrorStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class StoreValidator { + private final StoreRepository storeRepository; + + public Store validateStoreOwner(Long storeId, String email) { + Store store = storeRepository.findById(storeId) + .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); + + if(store.getOwner() == null || !store.getOwner().getEmail().equals(email)) { + throw new StoreException(StoreErrorStatus._NOT_STORE_OWNER); + } + + return store; + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/domain/storetable/controller/StoreTableController.java b/src/main/java/com/eatsfine/eatsfine/domain/storetable/controller/StoreTableController.java index aa51b4be..feff1a58 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/storetable/controller/StoreTableController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/storetable/controller/StoreTableController.java @@ -6,18 +6,20 @@ import com.eatsfine.eatsfine.domain.storetable.service.StoreTableCommandService; import com.eatsfine.eatsfine.domain.storetable.service.StoreTableQueryService; import com.eatsfine.eatsfine.domain.tableimage.status.TableImageSuccessStatus; +import com.eatsfine.eatsfine.global.annotation.CurrentUser; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.userdetails.User; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.time.LocalDate; -import java.time.LocalDate; @Tag(name = "StoreTable", description = "가게 테이블 관리 API") @RestController @@ -28,19 +30,23 @@ public class StoreTableController implements StoreTableControllerDocs { private final StoreTableQueryService storeTableQueryService; @PostMapping("/stores/{storeId}/tables") + @PreAuthorize("hasRole('OWNER')") public ApiResponse createTable( @PathVariable Long storeId, - @RequestBody StoreTableReqDto.TableCreateDto dto - ) { - return ApiResponse.of(StoreTableSuccessStatus._TABLE_CREATED, storeTableCommandService.createTable(storeId, dto)); + @RequestBody @Valid StoreTableReqDto.TableCreateDto dto, + @CurrentUser User user + ) { + return ApiResponse.of(StoreTableSuccessStatus._TABLE_CREATED, storeTableCommandService.createTable(storeId, dto, user.getUsername())); } @PostMapping(value = "/stores/{storeId}/tables/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PreAuthorize("hasRole('OWNER')") public ApiResponse uploadTableImageTemp( @PathVariable Long storeId, - @RequestPart("image") MultipartFile file + @RequestPart("image") MultipartFile file, + @CurrentUser User user ) { - return ApiResponse.of(TableImageSuccessStatus._STORE_TABLE_IMAGE_UPLOAD_SUCCESS, storeTableCommandService.uploadTableImageTemp(storeId, file)); + return ApiResponse.of(TableImageSuccessStatus._STORE_TABLE_IMAGE_UPLOAD_SUCCESS, storeTableCommandService.uploadTableImageTemp(storeId, file, user.getUsername())); } @GetMapping("/stores/{storeId}/tables/{tableId}/slots") @@ -64,39 +70,47 @@ public ApiResponse getTableDetail( } @PatchMapping("/stores/{storeId}/tables/{tableId}") + @PreAuthorize("hasRole('OWNER')") public ApiResponse updateTable( @PathVariable Long storeId, @PathVariable Long tableId, - @RequestBody @Valid StoreTableReqDto.TableUpdateDto dto + @RequestBody @Valid StoreTableReqDto.TableUpdateDto dto, + @CurrentUser User user ) { - return ApiResponse.of(StoreTableSuccessStatus._TABLE_UPDATED, storeTableCommandService.updateTable(storeId, tableId, dto)); + return ApiResponse.of(StoreTableSuccessStatus._TABLE_UPDATED, storeTableCommandService.updateTable(storeId, tableId, dto, user.getUsername())); } @DeleteMapping("/stores/{storeId}/tables/{tableId}") + @PreAuthorize("hasRole('OWNER')") public ApiResponse deleteTable( @PathVariable Long storeId, - @PathVariable Long tableId + @PathVariable Long tableId, + @CurrentUser User user ) { - return ApiResponse.of(StoreTableSuccessStatus._TABLE_DELETED, storeTableCommandService.deleteTable(storeId, tableId)); + return ApiResponse.of(StoreTableSuccessStatus._TABLE_DELETED, storeTableCommandService.deleteTable(storeId, tableId, user.getUsername())); } @PostMapping( value = "/stores/{storeId}/tables/{tableId}/table-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE ) + @PreAuthorize("hasRole('OWNER')") public ApiResponse uploadTableImage( @PathVariable Long storeId, @PathVariable Long tableId, - @RequestPart("tableImage") MultipartFile tableImage + @RequestPart("tableImage") MultipartFile tableImage, + @CurrentUser User user ) { - return ApiResponse.of(TableImageSuccessStatus._STORE_TABLE_IMAGE_UPLOAD_SUCCESS, storeTableCommandService.uploadTableImage(storeId, tableId, tableImage)); + return ApiResponse.of(TableImageSuccessStatus._STORE_TABLE_IMAGE_UPLOAD_SUCCESS, storeTableCommandService.uploadTableImage(storeId, tableId, tableImage, user.getUsername())); } @DeleteMapping("/stores/{storeId}/tables/{tableId}/table-image") + @PreAuthorize("hasRole('OWNER')") public ApiResponse deleteTableImage( @PathVariable Long storeId, - @PathVariable Long tableId + @PathVariable Long tableId, + @CurrentUser User user ) { - return ApiResponse.of(TableImageSuccessStatus._STORE_TABLE_IMAGE_DELETE_SUCCESS, storeTableCommandService.deleteTableImage(storeId, tableId)); + return ApiResponse.of(TableImageSuccessStatus._STORE_TABLE_IMAGE_DELETE_SUCCESS, storeTableCommandService.deleteTableImage(storeId, tableId, user.getUsername())); } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/storetable/controller/StoreTableControllerDocs.java b/src/main/java/com/eatsfine/eatsfine/domain/storetable/controller/StoreTableControllerDocs.java index 979707f3..71378537 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/storetable/controller/StoreTableControllerDocs.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/storetable/controller/StoreTableControllerDocs.java @@ -10,6 +10,7 @@ import jakarta.validation.Valid; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.MediaType; +import org.springframework.security.core.userdetails.User; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; @@ -40,7 +41,8 @@ public interface StoreTableControllerDocs { ApiResponse createTable( @Parameter(description = "가게 ID", required = true, example = "1") Long storeId, - @RequestBody @Valid StoreTableReqDto.TableCreateDto dto + @RequestBody @Valid StoreTableReqDto.TableCreateDto dto, + @Parameter(hidden = true) User user ); @Operation( @@ -68,7 +70,8 @@ ApiResponse uploadTableImageTemp( required = true, content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE) ) - MultipartFile file + MultipartFile file, + @Parameter(hidden = true) User user ); @Operation( @@ -194,7 +197,9 @@ ApiResponse updateTable( @Parameter(description = "테이블 ID", required = true, example = "1") Long tableId, - @RequestBody @Valid StoreTableReqDto.TableUpdateDto dto + @RequestBody @Valid StoreTableReqDto.TableUpdateDto dto, + + @Parameter(hidden = true) User user ); @Operation( @@ -218,7 +223,8 @@ ApiResponse deleteTable( @Parameter(description = "가게 ID", required = true, example = "1") Long storeId, @Parameter(description = "테이블 ID", required = true, example = "1") - Long tableId + Long tableId, + @Parameter(hidden = true) User user ); @Operation( @@ -249,7 +255,9 @@ ApiResponse uploadTableImage( required = true, content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE) ) - MultipartFile tableImage + MultipartFile tableImage, + + @Parameter(hidden = true) User user ); @Operation( @@ -272,6 +280,8 @@ ApiResponse deleteTableImage( Long storeId, @Parameter(description = "테이블 ID", required = true, example = "1") - Long tableId + Long tableId, + + @Parameter(hidden = true) User user ); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/storetable/service/StoreTableCommandService.java b/src/main/java/com/eatsfine/eatsfine/domain/storetable/service/StoreTableCommandService.java index 9106de98..a37bb837 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/storetable/service/StoreTableCommandService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/storetable/service/StoreTableCommandService.java @@ -5,15 +5,15 @@ import org.springframework.web.multipart.MultipartFile; public interface StoreTableCommandService { - StoreTableResDto.TableCreateDto createTable(Long storeId, StoreTableReqDto.TableCreateDto dto); + StoreTableResDto.TableCreateDto createTable(Long storeId, StoreTableReqDto.TableCreateDto dto, String email); - StoreTableResDto.ImageUploadDto uploadTableImageTemp(Long storeId, MultipartFile file); + StoreTableResDto.ImageUploadDto uploadTableImageTemp(Long storeId, MultipartFile file, String email); - StoreTableResDto.TableUpdateResultDto updateTable(Long storeId, Long tableId, StoreTableReqDto.TableUpdateDto dto); + StoreTableResDto.TableUpdateResultDto updateTable(Long storeId, Long tableId, StoreTableReqDto.TableUpdateDto dto, String email); - StoreTableResDto.TableDeleteDto deleteTable(Long storeId, Long tableId); + StoreTableResDto.TableDeleteDto deleteTable(Long storeId, Long tableId, String email); - StoreTableResDto.UploadTableImageDto uploadTableImage(Long storeId, Long tableId, MultipartFile tableImage); + StoreTableResDto.UploadTableImageDto uploadTableImage(Long storeId, Long tableId, MultipartFile tableImage, String email); - StoreTableResDto.DeleteTableImageDto deleteTableImage(Long storeId, Long tableId); + StoreTableResDto.DeleteTableImageDto deleteTableImage(Long storeId, Long tableId, String email); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/storetable/service/StoreTableCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/storetable/service/StoreTableCommandServiceImpl.java index e59ad840..9a82d5de 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/storetable/service/StoreTableCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/storetable/service/StoreTableCommandServiceImpl.java @@ -6,6 +6,7 @@ import com.eatsfine.eatsfine.domain.store.exception.StoreException; import com.eatsfine.eatsfine.domain.store.repository.StoreRepository; import com.eatsfine.eatsfine.domain.store.status.StoreErrorStatus; +import com.eatsfine.eatsfine.domain.store.validator.StoreValidator; import com.eatsfine.eatsfine.domain.storetable.converter.StoreTableConverter; import com.eatsfine.eatsfine.domain.storetable.dto.req.StoreTableReqDto; import com.eatsfine.eatsfine.domain.storetable.dto.res.StoreTableResDto; @@ -45,12 +46,12 @@ public class StoreTableCommandServiceImpl implements StoreTableCommandService { private final StoreTableRepository storeTableRepository; private final BookingRepository bookingRepository; private final S3Service s3Service; + private final StoreValidator storeValidator; // 테이블 생성 @Override - public StoreTableResDto.TableCreateDto createTable(Long storeId, StoreTableReqDto.TableCreateDto dto) { - storeRepository.findById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); + public StoreTableResDto.TableCreateDto createTable(Long storeId, StoreTableReqDto.TableCreateDto dto, String email) { + storeValidator.validateStoreOwner(storeId, email); TableLayout layout = tableLayoutRepository.findByStoreIdAndIsActiveTrue(storeId) .orElseThrow(() -> new TableLayoutException(TableLayoutErrorStatus._LAYOUT_NOT_FOUND)); @@ -119,9 +120,9 @@ public void afterCommit() { } @Override - public StoreTableResDto.ImageUploadDto uploadTableImageTemp(Long storeId, MultipartFile file) { - storeRepository.findById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); + public StoreTableResDto.ImageUploadDto uploadTableImageTemp(Long storeId, MultipartFile file, String email) { + + storeValidator.validateStoreOwner(storeId, email); if (file.isEmpty()) { throw new ImageException(ImageErrorStatus.EMPTY_FILE); @@ -136,15 +137,20 @@ public StoreTableResDto.ImageUploadDto uploadTableImageTemp(Long storeId, Multip // 테이블 정보 수정 @Override - public StoreTableResDto.TableUpdateResultDto updateTable(Long storeId, Long tableId, StoreTableReqDto.TableUpdateDto dto) { + public StoreTableResDto.TableUpdateResultDto updateTable( + Long storeId, + Long tableId, + StoreTableReqDto.TableUpdateDto dto, + String email + ) + { + storeValidator.validateStoreOwner(storeId, email); + // 최소 하나의 변경사항이 있는지 확인 if (!dto.hasAnyUpdate()) { throw new StoreTableException(StoreTableErrorStatus._NO_UPDATE_FIELD); } - storeRepository.findById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); - StoreTable table = storeTableRepository.findById(tableId) .orElseThrow(() -> new StoreTableException(StoreTableErrorStatus._TABLE_NOT_FOUND)); @@ -192,9 +198,9 @@ public StoreTableResDto.TableUpdateResultDto updateTable(Long storeId, Long tabl // 테이블 삭제 @Override - public StoreTableResDto.TableDeleteDto deleteTable(Long storeId, Long tableId) { - storeRepository.findById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); + public StoreTableResDto.TableDeleteDto deleteTable(Long storeId, Long tableId, String email) { + + storeValidator.validateStoreOwner(storeId, email); StoreTable table = storeTableRepository.findById(tableId) .orElseThrow(() -> new StoreTableException(StoreTableErrorStatus._TABLE_NOT_FOUND)); @@ -231,9 +237,14 @@ public void afterCommit() { // 테이블 이미지 업로드 @Override - public StoreTableResDto.UploadTableImageDto uploadTableImage(Long storeId, Long tableId, MultipartFile tableImage) { - storeRepository.findById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); + public StoreTableResDto.UploadTableImageDto uploadTableImage( + Long storeId, + Long tableId, + MultipartFile tableImage, + String email + ) { + + storeValidator.validateStoreOwner(storeId, email); StoreTable table = storeTableRepository.findById(tableId) .orElseThrow(() -> new StoreTableException(StoreTableErrorStatus._TABLE_NOT_FOUND)); @@ -260,9 +271,9 @@ public StoreTableResDto.UploadTableImageDto uploadTableImage(Long storeId, Long } @Override - public StoreTableResDto.DeleteTableImageDto deleteTableImage(Long storeId, Long tableId) { - storeRepository.findById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); + public StoreTableResDto.DeleteTableImageDto deleteTableImage(Long storeId, Long tableId, String email) { + + storeValidator.validateStoreOwner(storeId, email); StoreTable table = storeTableRepository.findById(tableId) .orElseThrow(() -> new StoreTableException(StoreTableErrorStatus._TABLE_NOT_FOUND)); diff --git a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/controller/TableLayoutController.java b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/controller/TableLayoutController.java index b96960d1..7b666b43 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/controller/TableLayoutController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/controller/TableLayoutController.java @@ -5,9 +5,12 @@ import com.eatsfine.eatsfine.domain.table_layout.exception.status.TableLayoutSuccessStatus; import com.eatsfine.eatsfine.domain.table_layout.service.TableLayoutCommandService; import com.eatsfine.eatsfine.domain.table_layout.service.TableLayoutQueryService; +import com.eatsfine.eatsfine.global.annotation.CurrentUser; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.userdetails.User; import org.springframework.web.bind.annotation.*; @Tag(name = "TableLayout", description = "테이블 배치도 조회 및 관리 API") @@ -19,16 +22,22 @@ public class TableLayoutController implements TableLayoutControllerDocs{ private final TableLayoutQueryService tableLayoutQueryService; @PostMapping("stores/{storeId}/layouts") + @PreAuthorize("hasRole('OWNER')") public ApiResponse createLayout( @PathVariable Long storeId, - @RequestBody TableLayoutReqDto.LayoutCreateDto dto + @RequestBody TableLayoutReqDto.LayoutCreateDto dto, + @CurrentUser User user ) { - return ApiResponse.of(TableLayoutSuccessStatus._LAYOUT_CREATED, tableLayoutCommandService.createLayout(storeId, dto)); + return ApiResponse.of(TableLayoutSuccessStatus._LAYOUT_CREATED, tableLayoutCommandService.createLayout(storeId, dto, user.getUsername())); } @GetMapping("stores/{storeId}/layouts") - public ApiResponse getActiveLayout(@PathVariable Long storeId) { - TableLayoutResDto.LayoutDetailDto result = tableLayoutQueryService.getActiveLayout(storeId); + @PreAuthorize("hasRole('OWNER')") + public ApiResponse getActiveLayout( + @PathVariable Long storeId, + @CurrentUser User user + ) { + TableLayoutResDto.LayoutDetailDto result = tableLayoutQueryService.getActiveLayout(storeId, user.getUsername()); if (result == null) { return ApiResponse.of(TableLayoutSuccessStatus._LAYOUT_NO_CONTENT, null); diff --git a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/controller/TableLayoutControllerDocs.java b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/controller/TableLayoutControllerDocs.java index 4a845671..2e0262d7 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/controller/TableLayoutControllerDocs.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/controller/TableLayoutControllerDocs.java @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; +import org.springframework.security.core.userdetails.User; public interface TableLayoutControllerDocs { @Operation( @@ -28,7 +29,8 @@ public interface TableLayoutControllerDocs { ApiResponse createLayout( @Parameter(description = "가게 ID", required = true, example = "1") Long storeId, - @RequestBody @Valid TableLayoutReqDto.LayoutCreateDto dto + @RequestBody @Valid TableLayoutReqDto.LayoutCreateDto dto, + @Parameter(hidden = true) User user ); @Operation( @@ -48,6 +50,7 @@ ApiResponse createLayout( }) ApiResponse getActiveLayout( @Parameter(description = "가게 ID", required = true, example = "1") - Long storeId + Long storeId, + @Parameter(hidden = true) User user ); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutCommandService.java b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutCommandService.java index 3e7840b4..28e4cc37 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutCommandService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutCommandService.java @@ -4,5 +4,5 @@ import com.eatsfine.eatsfine.domain.table_layout.dto.res.TableLayoutResDto; public interface TableLayoutCommandService { - TableLayoutResDto.LayoutDetailDto createLayout(Long storeId, TableLayoutReqDto.LayoutCreateDto dto); + TableLayoutResDto.LayoutDetailDto createLayout(Long storeId, TableLayoutReqDto.LayoutCreateDto dto, String email); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutCommandServiceImpl.java index 3eeb906e..ced9c523 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutCommandServiceImpl.java @@ -4,6 +4,7 @@ import com.eatsfine.eatsfine.domain.store.exception.StoreException; import com.eatsfine.eatsfine.domain.store.repository.StoreRepository; import com.eatsfine.eatsfine.domain.store.status.StoreErrorStatus; +import com.eatsfine.eatsfine.domain.store.validator.StoreValidator; import com.eatsfine.eatsfine.domain.table_layout.converter.TableLayoutConverter; import com.eatsfine.eatsfine.domain.table_layout.dto.req.TableLayoutReqDto; import com.eatsfine.eatsfine.domain.table_layout.dto.res.TableLayoutResDto; @@ -19,12 +20,17 @@ public class TableLayoutCommandServiceImpl implements TableLayoutCommandService { private final StoreRepository storeRepository; private final TableLayoutRepository tableLayoutRepository; + private final StoreValidator storeValidator; // 테이블 배치도 생성 @Override - public TableLayoutResDto.LayoutDetailDto createLayout(Long storeId, TableLayoutReqDto.LayoutCreateDto dto) { - Store store = storeRepository.findById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); + public TableLayoutResDto.LayoutDetailDto createLayout( + Long storeId, + TableLayoutReqDto.LayoutCreateDto dto, + String email + ) { + + Store store = storeValidator.validateStoreOwner(storeId, email); deactivateExistingLayout(store); diff --git a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutQueryService.java b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutQueryService.java index ac834759..ec97f7eb 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutQueryService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutQueryService.java @@ -3,5 +3,5 @@ import com.eatsfine.eatsfine.domain.table_layout.dto.res.TableLayoutResDto; public interface TableLayoutQueryService { - TableLayoutResDto.LayoutDetailDto getActiveLayout(Long storeId); + TableLayoutResDto.LayoutDetailDto getActiveLayout(Long storeId, String email); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutQueryServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutQueryServiceImpl.java index b5ab0c68..811bd3b2 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutQueryServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/table_layout/service/TableLayoutQueryServiceImpl.java @@ -3,6 +3,7 @@ import com.eatsfine.eatsfine.domain.store.exception.StoreException; import com.eatsfine.eatsfine.domain.store.repository.StoreRepository; import com.eatsfine.eatsfine.domain.store.status.StoreErrorStatus; +import com.eatsfine.eatsfine.domain.store.validator.StoreValidator; import com.eatsfine.eatsfine.domain.table_layout.converter.TableLayoutConverter; import com.eatsfine.eatsfine.domain.table_layout.dto.res.TableLayoutResDto; import com.eatsfine.eatsfine.domain.table_layout.repository.TableLayoutRepository; @@ -16,12 +17,13 @@ public class TableLayoutQueryServiceImpl implements TableLayoutQueryService { private final StoreRepository storeRepository; private final TableLayoutRepository tableLayoutRepository; + private final StoreValidator storeValidator; // 테이블 배치도 조회 @Override - public TableLayoutResDto.LayoutDetailDto getActiveLayout(Long storeId) { - storeRepository.findById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); + public TableLayoutResDto.LayoutDetailDto getActiveLayout(Long storeId, String email) { + + storeValidator.validateStoreOwner(storeId, email); // 배치도가 없을 시 null 반환 return tableLayoutRepository.findByStoreIdAndIsActiveTrue(storeId) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/tableblock/controller/TableBlockController.java b/src/main/java/com/eatsfine/eatsfine/domain/tableblock/controller/TableBlockController.java index 0d9d2f07..b98e34a2 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/tableblock/controller/TableBlockController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/tableblock/controller/TableBlockController.java @@ -4,9 +4,12 @@ import com.eatsfine.eatsfine.domain.tableblock.dto.res.TableBlockResDto; import com.eatsfine.eatsfine.domain.tableblock.exception.status.TableBlockSuccessStatus; import com.eatsfine.eatsfine.domain.tableblock.service.TableBlockCommandService; +import com.eatsfine.eatsfine.global.annotation.CurrentUser; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.userdetails.User; import org.springframework.web.bind.annotation.*; @Tag(name = "TableBlock", description = "테이블 슬롯 차단/해제 API") @@ -18,11 +21,13 @@ public class TableBlockController implements TableBlockControllerDocs { @Override @PatchMapping("/stores/{storeId}/tables/{tableId}/slots") + @PreAuthorize("hasRole('OWNER')") public ApiResponse updateSlotStatus( @PathVariable Long storeId, @PathVariable Long tableId, - @RequestBody TableBlockReqDto.SlotStatusUpdateDto dto - ) { - return ApiResponse.of(TableBlockSuccessStatus._SLOT_STATUS_UPDATED, tableBlockCommandService.updateSlotStatus(storeId, tableId, dto)); + @RequestBody TableBlockReqDto.SlotStatusUpdateDto dto, + @CurrentUser User user + ) { + return ApiResponse.of(TableBlockSuccessStatus._SLOT_STATUS_UPDATED, tableBlockCommandService.updateSlotStatus(storeId, tableId, dto, user.getUsername())); } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/tableblock/controller/TableBlockControllerDocs.java b/src/main/java/com/eatsfine/eatsfine/domain/tableblock/controller/TableBlockControllerDocs.java index ace32da1..0d7052ac 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/tableblock/controller/TableBlockControllerDocs.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/tableblock/controller/TableBlockControllerDocs.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; +import org.springframework.security.core.userdetails.User; import org.springframework.web.bind.annotation.RequestBody; public interface TableBlockControllerDocs { @@ -31,6 +32,9 @@ ApiResponse updateSlotStatus( @Parameter(description = "테이블 ID", required = true, example = "1") Long tableId, - @RequestBody @Valid TableBlockReqDto.SlotStatusUpdateDto dto + @RequestBody @Valid TableBlockReqDto.SlotStatusUpdateDto dto, + + @Parameter(hidden = true) User user + ); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/tableblock/service/TableBlockCommandService.java b/src/main/java/com/eatsfine/eatsfine/domain/tableblock/service/TableBlockCommandService.java index 0add9f8c..6a902332 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/tableblock/service/TableBlockCommandService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/tableblock/service/TableBlockCommandService.java @@ -4,5 +4,5 @@ import com.eatsfine.eatsfine.domain.tableblock.dto.res.TableBlockResDto; public interface TableBlockCommandService { - TableBlockResDto.SlotStatusUpdateDto updateSlotStatus(Long storeId, Long tableId, TableBlockReqDto.SlotStatusUpdateDto dto); + TableBlockResDto.SlotStatusUpdateDto updateSlotStatus(Long storeId, Long tableId, TableBlockReqDto.SlotStatusUpdateDto dto, String email); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/tableblock/service/TableBlockCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/tableblock/service/TableBlockCommandServiceImpl.java index a0af490e..5a454e16 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/tableblock/service/TableBlockCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/tableblock/service/TableBlockCommandServiceImpl.java @@ -5,6 +5,7 @@ import com.eatsfine.eatsfine.domain.store.exception.StoreException; import com.eatsfine.eatsfine.domain.store.repository.StoreRepository; import com.eatsfine.eatsfine.domain.store.status.StoreErrorStatus; +import com.eatsfine.eatsfine.domain.store.validator.StoreValidator; import com.eatsfine.eatsfine.domain.storetable.entity.StoreTable; import com.eatsfine.eatsfine.domain.storetable.exception.StoreTableException; import com.eatsfine.eatsfine.domain.storetable.exception.status.StoreTableErrorStatus; @@ -34,12 +35,18 @@ public class TableBlockCommandServiceImpl implements TableBlockCommandService { private final TableBlockRepository tableBlockRepository; private final StoreRepository storeRepository; private final BookingRepository bookingRepository; + private final StoreValidator storeValidator; // 테이블 슬롯 상태 변경 @Override - public TableBlockResDto.SlotStatusUpdateDto updateSlotStatus(Long storeId, Long tableId, TableBlockReqDto.SlotStatusUpdateDto dto) { - Store store = storeRepository.findById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); + public TableBlockResDto.SlotStatusUpdateDto updateSlotStatus( + Long storeId, + Long tableId, + TableBlockReqDto.SlotStatusUpdateDto dto, + String email + ) { + + Store store = storeValidator.validateStoreOwner(storeId, email); StoreTable table = storeTableRepository.findById(tableId) .orElseThrow(() -> new StoreTableException(StoreTableErrorStatus._TABLE_NOT_FOUND)); diff --git a/src/main/java/com/eatsfine/eatsfine/domain/tableimage/controller/TableImageController.java b/src/main/java/com/eatsfine/eatsfine/domain/tableimage/controller/TableImageController.java index 7ff69f6c..d82901d1 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/tableimage/controller/TableImageController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/tableimage/controller/TableImageController.java @@ -4,11 +4,14 @@ import com.eatsfine.eatsfine.domain.tableimage.service.TableImageCommandService; import com.eatsfine.eatsfine.domain.tableimage.service.TableImageQueryService; import com.eatsfine.eatsfine.domain.tableimage.status.TableImageSuccessStatus; +import com.eatsfine.eatsfine.global.annotation.CurrentUser; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.userdetails.User; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -31,13 +34,15 @@ public class TableImageController { value = "/stores/{storeId}/table-images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE ) + @PreAuthorize("hasRole('OWNER')") ApiResponse uploadTableImage( @RequestPart("file") List files, - @PathVariable Long storeId - ) { + @PathVariable Long storeId, + @CurrentUser User user + ) { return ApiResponse.of( TableImageSuccessStatus._STORE_TABLE_IMAGE_UPLOAD_SUCCESS, - tableImageCommandService.uploadTableImage(storeId, files) + tableImageCommandService.uploadTableImage(storeId, files, user.getUsername()) ); } @@ -46,7 +51,7 @@ ApiResponse uploadTableImage( description = "식당 테이블 이미지들을 조회합니다." ) @GetMapping("/stores/{storeId}/table-images") - ApiResponse getTableImage( + ApiResponse getTableImage( @PathVariable Long storeId ) { return ApiResponse.of(TableImageSuccessStatus._STORE_TABLE_IMAGE_GET_SUCCESS, tableImageQueryService.getTableImage(storeId)); @@ -57,11 +62,13 @@ ApiResponse getTableImage( description = "식당 테이블 이미지를 삭제합니다." ) @DeleteMapping("/stores/{storeId}/table-images") + @PreAuthorize("hasRole('OWNER')") ApiResponse deleteTableImage( @PathVariable Long storeId, - @RequestBody List tableImageIds + @RequestBody List tableImageIds, + @CurrentUser User user ) { - return ApiResponse.of(TableImageSuccessStatus._STORE_TABLE_IMAGE_DELETE_SUCCESS, tableImageCommandService.deleteTableImage(storeId, tableImageIds)); + return ApiResponse.of(TableImageSuccessStatus._STORE_TABLE_IMAGE_DELETE_SUCCESS, tableImageCommandService.deleteTableImage(storeId, tableImageIds, user.getUsername())); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/tableimage/service/TableImageCommandService.java b/src/main/java/com/eatsfine/eatsfine/domain/tableimage/service/TableImageCommandService.java index be27464b..2077aa5c 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/tableimage/service/TableImageCommandService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/tableimage/service/TableImageCommandService.java @@ -7,7 +7,7 @@ public interface TableImageCommandService { - TableImageResDto.UploadTableImageDto uploadTableImage(Long storeId, List files); + TableImageResDto.UploadTableImageDto uploadTableImage(Long storeId, List files, String email); - TableImageResDto.DeleteTableImageDto deleteTableImage(Long storeId, List tableImageIds); + TableImageResDto.DeleteTableImageDto deleteTableImage(Long storeId, List tableImageIds, String email); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/tableimage/service/TableImageCommandServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/tableimage/service/TableImageCommandServiceImpl.java index bc72c8ba..a62e3084 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/tableimage/service/TableImageCommandServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/tableimage/service/TableImageCommandServiceImpl.java @@ -6,6 +6,7 @@ import com.eatsfine.eatsfine.domain.store.exception.StoreException; import com.eatsfine.eatsfine.domain.store.repository.StoreRepository; import com.eatsfine.eatsfine.domain.store.status.StoreErrorStatus; +import com.eatsfine.eatsfine.domain.store.validator.StoreValidator; import com.eatsfine.eatsfine.domain.tableimage.converter.TableImageConverter; import com.eatsfine.eatsfine.domain.tableimage.dto.TableImageResDto; import com.eatsfine.eatsfine.domain.tableimage.entity.TableImage; @@ -28,12 +29,12 @@ public class TableImageCommandServiceImpl implements TableImageCommandService { private final StoreRepository storeRepository; private final TableImageRepository tableImageRepository; private final S3Service s3Service; + private final StoreValidator storeValidator; // 가게 테이블 이미지 등록 - public TableImageResDto.UploadTableImageDto uploadTableImage(Long storeId, List files) { + public TableImageResDto.UploadTableImageDto uploadTableImage(Long storeId, List files, String email) { - Store store = storeRepository.findById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); + Store store = storeValidator.validateStoreOwner(storeId, email); if(files == null || files.isEmpty() || files.stream().allMatch(MultipartFile::isEmpty)) { throw new ImageException(ImageErrorStatus.EMPTY_FILE); @@ -55,9 +56,9 @@ public TableImageResDto.UploadTableImageDto uploadTableImage(Long storeId, List< } @Override - public TableImageResDto.DeleteTableImageDto deleteTableImage(Long storeId, List tableImageIds) { - Store store = storeRepository.findById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorStatus._STORE_NOT_FOUND)); + public TableImageResDto.DeleteTableImageDto deleteTableImage(Long storeId, List tableImageIds, String email) { + + Store store = storeValidator.validateStoreOwner(storeId, email); List tableImages = tableImageIds.stream() .map(id -> tableImageRepository.findByIdAndStore(id, store) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java index c8332407..5ac0c596 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java @@ -4,6 +4,8 @@ import com.eatsfine.eatsfine.domain.user.service.authService.AuthTokenService; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -15,6 +17,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "RefreshToken", description = "refreshToken 재발급 API") @Slf4j @RestController @RequiredArgsConstructor @@ -25,13 +29,14 @@ public class AuthController { private final AuthCookieProvider authCookieProvider; @PostMapping("/reissue") + @Operation(summary = "재발급 API", description = "refreshToken을 재발급 하는 API입니다.") public ResponseEntity> reissue( @CookieValue(value = "refreshToken", required = false) String refreshToken, HttpServletResponse response ) { log.info("[REISSUE API] 재발급 요청 받음. refreshToken={}", refreshToken); - AuthTokenService.ReissueResult result = authTokenService.reissue(refreshToken); + AuthTokenService.ReissueResult result = authTokenService.reissue(refreshToken, user.getRole()); ResponseCookie refreshCookie = authCookieProvider.refreshTokenCookie(result.refreshToken()); response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString()); diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java index 27c3864e..af0ceeea 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java @@ -3,9 +3,10 @@ import com.eatsfine.eatsfine.domain.user.dto.request.UserRequestDto; import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; -import com.eatsfine.eatsfine.domain.user.exception.UserException; +import com.eatsfine.eatsfine.domain.user.exception.AuthException; import com.eatsfine.eatsfine.domain.user.service.userService.UserService; -import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; +import com.eatsfine.eatsfine.domain.user.status.AuthErrorStatus; +import com.eatsfine.eatsfine.domain.user.status.UserSuccessStatus; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; @@ -48,7 +49,7 @@ public ResponseEntity> login(@Requ UserResponseDto.LoginResponseDto loginResult = userService.login(loginDto); if (loginResult.getRefreshToken() == null || loginResult.getRefreshToken().isBlank()) { - throw new UserException(UserErrorStatus.REFRESH_TOKEN_NOT_ISSUED); + throw new AuthException(AuthErrorStatus.REFRESH_TOKEN_NOT_ISSUED); } ResponseCookie refreshCookie = authCookieProvider.refreshTokenCookie(loginResult.getRefreshToken()); @@ -105,6 +106,20 @@ public ResponseEntity> updateProfileImage( } + @PatchMapping("/api/users/role/owner") + @Operation( + summary = "사장 인증 API - 인증 필요", + description = "사장 인증을 통해 사장 권한을 부여받습니다.", + security = {@SecurityRequirement(name = "JWT")} + ) + public ApiResponse verifyOwner( + @RequestBody @Valid UserRequestDto.VerifyOwnerDto verifyOwnerDto, + HttpServletRequest request + ) { + return ApiResponse.of(UserSuccessStatus.OWNER_VERIFICATION_SUCCESS, userService.verifyOwner(verifyOwnerDto, request)); + } + + @DeleteMapping("/api/auth/withdraw") @Operation( summary = "회원 탈퇴 API - 인증 필요", @@ -136,4 +151,4 @@ public ResponseEntity> logout(HttpServletRequest request) { .body(ApiResponse.onSuccess("로그아웃이 되었습니다.")); } -} +} \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java index 00ac3667..c8f9d4bc 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java @@ -7,7 +7,6 @@ import com.eatsfine.eatsfine.domain.user.enums.SocialType; import java.time.LocalDateTime; - import static com.eatsfine.eatsfine.domain.user.enums.Role.ROLE_CUSTOMER; public class UserConverter { @@ -20,7 +19,7 @@ public static UserResponseDto.JoinResultDto toJoinResult(User user) { } - //로그인 응답 변환 + //로그인 응답 변환 public static UserResponseDto.LoginResponseDto toLoginResponse(User user, String accessToken) { return UserResponseDto.LoginResponseDto.builder() .id(user.getId()) @@ -36,7 +35,7 @@ public static UserResponseDto.UserInfoDto toUserInfo(User user) { .id(user.getId()) .profileImage(user.getProfileImage()) .email(user.getEmail()) - .nickName(user.getNickName()) + .name(user.getName()) .phoneNumber(user.getPhoneNumber()) .build(); } @@ -47,7 +46,7 @@ public static UserResponseDto.UpdateResponseDto toUpdateResponse(User user) { return UserResponseDto.UpdateResponseDto.builder() .profileImage(user.getProfileImage()) .email(user.getEmail()) - .nickName(user.getNickName()) + .name(user.getName()) .phoneNumber(user.getPhoneNumber()) .build(); } @@ -65,7 +64,7 @@ public static UserResponseDto.UpdatePasswordDto toUpdatePasswordResponse(boolean public static User toUser(UserRequestDto.JoinDto dto, String encodedPassword) { return User.builder() - .nickName(dto.getNickName()) + .name(dto.getName()) .email(dto.getEmail()) .phoneNumber(dto.getPhoneNumber()) .password(encodedPassword) @@ -83,6 +82,13 @@ public static Term toUserTerm(UserRequestDto.JoinDto dto, User user) { } + public static UserResponseDto.VerifyOwnerDto toVerifyOwnerResponse(User user) { + return UserResponseDto.VerifyOwnerDto.builder() + .userId(user.getId()) + .build(); + } + + /* 소셜 유저 생성 (최초 소셜 가입 등) -소셜 로그인에서 email/nickname/phoneNumber 등을 확보한 후 엔티티 생성에 사용 @@ -91,7 +97,7 @@ public static User toSocialUser(String email, String nickName, String profileIma return User.builder() .email(email) - .nickName(nickName) + .name(nickName) .profileImage(profileImage) .socialId(socialId) .socialType(socialType) @@ -100,4 +106,4 @@ public static User toSocialUser(String email, String nickName, String profileIma .build(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java index c8ea733a..8315f69f 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java @@ -3,8 +3,8 @@ import com.eatsfine.eatsfine.global.validator.annotation.PasswordMatch; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; -import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; public class UserRequestDto { @@ -12,9 +12,8 @@ public class UserRequestDto { @PasswordMatch @Getter public static class JoinDto{ - @NotBlank(message = "이름은 필수입니다.") - private String nickName; // 이름 + private String name; // 이름 @NotBlank(message = "이메일은 필수입니다.") @Email(message = "유효한 이메일 형식이어야 합니다.") @@ -62,7 +61,7 @@ public static class LoginDto { @Setter public static class UpdateDto { private String email; - private String nickName; + private String name; private String phoneNumber; } @@ -86,4 +85,20 @@ public static class ChangePasswordDto { @Schema(description = "새 비밀번호 확인", example = "NewPw!1234") private String newPasswordConfirm; } + + @Getter + @NoArgsConstructor + public static class VerifyOwnerDto { + + @Schema(description = "사업자번호", example = "1234567890") + @NotBlank(message = "사업자번호는 필수입니다.") + @Pattern(regexp = "^[0-9]{10}$", message = "사업자번호는 숫자 10자리여야 합니다.") + private String businessNumber; + + @Schema(description = "개업일자", example = "20240101") + @NotBlank(message = "개업일자는 필수입니다.") + @Pattern(regexp = "^[0-9]{8}$", message = "개업일자는 YYYYMMDD 형식이어야 합니다.") + private String startDate; + } + } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java index 5253ed9f..52dbbbd1 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java @@ -34,7 +34,7 @@ public static class UserInfoDto{ private Long id; private String profileImage; private String email; - private String nickName; + private String name; private String phoneNumber; } @@ -44,7 +44,7 @@ public static class UserInfoDto{ public static class UpdateResponseDto{ private String profileImage; private String email; - private String nickName; + private String name; private String phoneNumber; } @@ -69,4 +69,9 @@ public static class AccessTokenResponse { private String accessToken; } + @Builder + public static class VerifyOwnerDto { + @Schema(description = "권한 승격이 완료된 유저의 식별자", example = "1") + private Long userId; + } } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java index d628643f..fb7a2cbb 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java @@ -5,7 +5,6 @@ import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; - @Entity @Getter // 수정한 부분: access 레벨을 PROTECTED로 설정하여 Hibernate가 접근할 수 있게 합니다. @@ -20,16 +19,17 @@ public class User extends BaseEntity { private Long id; @Column(nullable = false, length = 20) - private String nickName; + private String name; @Column(nullable = false, unique = true) private String email; private String password; - @Column(nullable = true, length = 20) + @Column(nullable = false, length = 20) private String phoneNumber; + @Getter @Enumerated(EnumType.STRING) private Role role; @@ -46,24 +46,33 @@ public class User extends BaseEntity { @Column(length = 500) private String refreshToken; - public void updateNickname(String nickName){ - this.nickName = nickName; + public void updateName(String name) { + this.name = name; } public void updatePhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } - public void updateEmail(String email) {this.email = email;} + public void updateEmail(String email) { + this.email = email; + } public void updateProfileImage(String profileImage) { this.profileImage = profileImage; } - public void updateRefreshToken(String refreshToken){this.refreshToken = refreshToken;} + public void updateToOwner() { + this.role = Role.ROLE_OWNER; + } + + public void updateRefreshToken(String refreshToken){ + this.refreshToken = refreshToken; + } public void linkSocial(SocialType socialType, String socialId) { this.socialType = socialType; this.socialId = socialId; } -} + +} \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java index c0a856e7..16a4e9e9 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java @@ -3,6 +3,8 @@ import com.eatsfine.eatsfine.domain.user.entity.User; import com.eatsfine.eatsfine.domain.user.enums.SocialType; import com.eatsfine.eatsfine.domain.user.repository.UserRepository; +import com.eatsfine.eatsfine.domain.user.status.AuthErrorStatus; +import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; import jakarta.servlet.http.HttpServletRequest; @@ -50,7 +52,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, socialType = SocialType.valueOf(provider.toUpperCase()); } catch (IllegalArgumentException e) { log.error("Unknown provider registrationId={}", provider, e); - redirectFail(response, "unknown_provider"); + redirectFail(response, AuthErrorStatus.OAUTH2_PROVIDER_NOT_SUPPORTED); return; } @@ -69,19 +71,19 @@ public void onAuthenticationSuccess(HttpServletRequest request, if (socialType == SocialType.KAKAO) { logKakaoAccountStatus(attrs); } - redirectFail(response, "email_not_found"); + redirectFail(response, AuthErrorStatus.OAUTH2_EMAIL_NOT_FOUND); return; } // DB에서 user 조회 User user = userRepository.findByEmail(email).orElse(null); if (user == null) { - redirectFail(response, "user_not_found"); + redirectFail(response, UserErrorStatus.MEMBER_NOT_FOUND); return; } // 토큰 발급 - String accessToken = jwtTokenProvider.createAccessToken(email); + String accessToken = jwtTokenProvider.createAccessToken(email, user.getRole().name()); String refreshToken = jwtTokenProvider.createRefreshToken(email); // refresh DB 저장 @@ -104,13 +106,27 @@ public void onAuthenticationSuccess(HttpServletRequest request, response.sendRedirect(redirectUrl); } - private void redirectFail(HttpServletResponse response, String errorCode) throws IOException { + private void redirectFail(HttpServletResponse response, AuthErrorStatus errorStatus) throws IOException { String failUrl = UriComponentsBuilder.fromUriString(LOGIN_ERROR_REDIRECT_BASE) - .queryParam("error", errorCode) + .queryParam("error", errorStatus.getCode()) + .queryParam("message", errorStatus.getMessage()) .build() .toUriString(); - log.warn("[OAuth2 FAIL] errorCode={}, failUrl={}", errorCode, failUrl); + log.warn("[OAuth2 FAIL] errorCode={}, message={}, failUrl={}", + errorStatus.getCode(), errorStatus.getMessage(), failUrl); + response.sendRedirect(failUrl); + } + + private void redirectFail(HttpServletResponse response, UserErrorStatus errorStatus) throws IOException { + String failUrl = UriComponentsBuilder.fromUriString(LOGIN_ERROR_REDIRECT_BASE) + .queryParam("error", errorStatus.getCode()) + .queryParam("message", errorStatus.getMessage()) + .build() + .toUriString(); + + log.warn("[OAuth2 FAIL] errorCode={}, message={}, failUrl={}", + errorStatus.getCode(), errorStatus.getMessage(), failUrl); response.sendRedirect(failUrl); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenService.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenService.java index 963b70cf..d7e2e479 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenService.java @@ -1,8 +1,10 @@ package com.eatsfine.eatsfine.domain.user.service.authService; +import com.eatsfine.eatsfine.domain.user.enums.Role; + public interface AuthTokenService { - ReissueResult reissue(String refreshToken); + ReissueResult reissue(String refreshToken, Role role); record ReissueResult(String accessToken, String refreshToken) {} } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java index b2ac8eb8..e0715a3d 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/authService/AuthTokenServiceImpl.java @@ -1,9 +1,11 @@ package com.eatsfine.eatsfine.domain.user.service.authService; import com.eatsfine.eatsfine.domain.user.entity.User; +import com.eatsfine.eatsfine.domain.user.enums.Role; import com.eatsfine.eatsfine.domain.user.exception.AuthException; import com.eatsfine.eatsfine.domain.user.repository.UserRepository; import com.eatsfine.eatsfine.domain.user.status.AuthErrorStatus; + import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; @@ -22,7 +24,7 @@ public class AuthTokenServiceImpl implements AuthTokenService { private final UserRepository userRepository; @Override - public ReissueResult reissue(String refreshToken) { + public ReissueResult reissue(String refreshToken, Role role) { log.info("[REISSUE] 재발급 요청 시작"); log.info("[REISSUE] 요청 refreshToken={}", refreshToken); @@ -73,7 +75,7 @@ public ReissueResult reissue(String refreshToken) { log.info("[REISSUE] 토큰 일치 확인 완료. 새 토큰 발급 시작"); // 새 토큰 발급 - String newAccessToken = jwtTokenProvider.createAccessToken(email); + String newAccessToken = jwtTokenProvider.createAccessToken(email, role.name()); String newRefreshToken = jwtTokenProvider.createRefreshToken(email); log.info("[REISSUE] 새 accessToken 발급 완료"); diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java index 6c81360d..665b8a1f 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java @@ -21,4 +21,6 @@ public interface UserService { void logout(HttpServletRequest request); + UserResponseDto.VerifyOwnerDto verifyOwner(UserRequestDto.VerifyOwnerDto verifyOwnerDto, HttpServletRequest request); + } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java index 5ccf66d3..bad3f3ae 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java @@ -1,6 +1,7 @@ package com.eatsfine.eatsfine.domain.user.service.userService; +import com.eatsfine.eatsfine.domain.businessnumber.validator.BusinessNumberValidator; import com.eatsfine.eatsfine.domain.image.exception.ImageException; import com.eatsfine.eatsfine.domain.image.status.ImageErrorStatus; import com.eatsfine.eatsfine.domain.term.repository.TermRepository; @@ -8,9 +9,12 @@ import com.eatsfine.eatsfine.domain.user.dto.request.UserRequestDto; import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; import com.eatsfine.eatsfine.domain.user.entity.User; +import com.eatsfine.eatsfine.domain.user.enums.Role; +import com.eatsfine.eatsfine.domain.user.exception.AuthException; import com.eatsfine.eatsfine.domain.user.exception.UserException; import com.eatsfine.eatsfine.domain.user.repository.UserRepository; import com.eatsfine.eatsfine.domain.user.service.userService.UserService; +import com.eatsfine.eatsfine.domain.user.status.AuthErrorStatus; import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; import com.eatsfine.eatsfine.global.s3.S3Service; @@ -33,6 +37,7 @@ public class UserServiceImpl implements UserService { private final PasswordEncoder passwordEncoder; private final JwtTokenProvider jwtTokenProvider; private final S3Service s3Service; + private final BusinessNumberValidator businessNumberValidator; @Override @Transactional @@ -67,7 +72,7 @@ public UserResponseDto.LoginResponseDto login(UserRequestDto.LoginDto loginDto) } // 3) 토큰 발급 - String accessToken = jwtTokenProvider.createAccessToken(user.getEmail()); + String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(), user.getRole().name()); String refreshToken = jwtTokenProvider.createRefreshToken(user.getEmail()); // 4) refreshToken 저장 @@ -97,13 +102,12 @@ public String updateMemberInfo(UserRequestDto.UpdateDto updateDto, boolean changed = false; - //닉네임/전화번호 부분 수정 - if (updateDto != null) { - if (updateDto.getNickName() != null && !updateDto.getNickName().isBlank()) { - user.updateNickname(updateDto.getNickName()); - changed = true; - } + // 이름/전화번호 부분 수정 + if (updateDto.getName() != null && !updateDto.getName().isBlank()) { + user.updateName(updateDto.getName()); + changed = true; } + if (updateDto.getPhoneNumber() != null && !updateDto.getPhoneNumber().isBlank()) { user.updatePhoneNumber(updateDto.getPhoneNumber()); changed = true; @@ -163,7 +167,7 @@ public void afterCommit() { log.info("[Service] Updated userId={}, nickname={}, phone={}, profileKey={}", user.getId(), - user.getNickName(), + user.getName(), user.getPhoneNumber(), user.getProfileImage()); @@ -213,7 +217,7 @@ public void logout(HttpServletRequest request) { private User getCurrentUser(HttpServletRequest request) { String token = JwtTokenProvider.resolveToken(request); if (token == null || token.isBlank() || !jwtTokenProvider.validateToken(token)) { - throw new UserException(UserErrorStatus.INVALID_TOKEN); + throw new AuthException(AuthErrorStatus.INVALID_TOKEN); } String email = jwtTokenProvider.getEmailFromToken(token); @@ -221,4 +225,25 @@ private User getCurrentUser(HttpServletRequest request) { return userRepository.findByEmail(email) .orElseThrow(() -> new UserException(UserErrorStatus.MEMBER_NOT_FOUND)); } + + @Override + @Transactional + public UserResponseDto.VerifyOwnerDto verifyOwner(UserRequestDto.VerifyOwnerDto dto, HttpServletRequest request) { + User user = getCurrentUser(request); + log.info("[OwnerAuth] 사장 인증 시도 - 유저ID: {}, 이메일: {}", + user.getId(), user.getEmail()); + + if (user.getRole() == Role.ROLE_OWNER) { + log.warn("[OwnerAuth] 인증 실패 - 이미 사장 권한을 가진 유저입니다. 유저ID: {}", user.getId()); + throw new AuthException(AuthErrorStatus.ALREADY_OWNER); + } + + businessNumberValidator.validate(dto.getBusinessNumber(), dto.getStartDate(), user.getName()); + + user.updateToOwner(); + User savedUser = userRepository.save(user); + + log.info("[OwnerAuth] 인증 성공 - 유저 권한이 OWNER로 변경되었습니다. 유저ID: {}", savedUser.getId()); + return UserConverter.toVerifyOwnerResponse(savedUser); + } } \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java index a3818292..f79c527d 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java @@ -14,10 +14,18 @@ public enum AuthErrorStatus implements BaseErrorCode { OAUTH2_PROVIDER_NOT_SUPPORTED(HttpStatus.BAD_REQUEST, "AUTH4002", "지원하지 않는 소셜 로그인 제공자입니다."), REFRESH_TOKEN_MISSING(HttpStatus.UNAUTHORIZED, "AUTH4005", "리프레시 토큰이 없습니다."), - + // 토큰 관련 에러 INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH4003", "유효하지 않은 토큰입니다."), EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH4004", "토큰이 만료되었습니다."), - REFRESH_TOKEN_NOT_ISSUED(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH5001", "리프레시 토큰이 발급되지 않았습니다."); + REFRESH_TOKEN_NOT_ISSUED(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH5001", "리프레시 토큰이 발급되지 않았습니다."), + + EMPTY_TOKEN_ROLE(HttpStatus.UNAUTHORIZED, "AUTH407", "토큰 내 권한 정보가 누락되었습니다."), + // 사장 인증 관련 에러 + ALREADY_OWNER(HttpStatus.CONFLICT, "OWNER409", "이미 사장 회원입니다."), + + // 사장 전용 API 접근 관련 에러 + FORBIDDEN_OWNER(HttpStatus.FORBIDDEN, "AUTH406", "사장님 권한이 필요한 서비스입니다.") + ; private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java index 6a00cbcd..a0c1544f 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java @@ -12,15 +12,13 @@ public enum UserErrorStatus implements BaseErrorCode { // 멤버 관련 에러 MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."), + NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다."), - EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER4003", "이미 존재하는 이메일입니다."), - INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4004", "비밀번호가 올바르지 않습니다."), - // 토큰 관련 에러 - INVALID_TOKEN(HttpStatus.NOT_FOUND, "TOKEN4001", "토큰이 없습니다."), - EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN4002", "토큰이 만료되었습니다."), - REFRESH_TOKEN_NOT_ISSUED(HttpStatus.INTERNAL_SERVER_ERROR, "TOKEN5001", "리프레시 토큰이 발급되지 않았습니다."); + NAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "이름은 필수 입니다."), + EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER4003", "이미 존재하는 이메일입니다."), + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4004", "비밀번호가 올바르지 않습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserSuccessStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserSuccessStatus.java new file mode 100644 index 00000000..f9f4ee55 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserSuccessStatus.java @@ -0,0 +1,40 @@ +package com.eatsfine.eatsfine.domain.user.status; + +import com.eatsfine.eatsfine.global.apiPayload.code.BaseCode; +import com.eatsfine.eatsfine.global.apiPayload.code.ErrorReasonDto; +import com.eatsfine.eatsfine.global.apiPayload.code.ReasonDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum UserSuccessStatus implements BaseCode { + + OWNER_VERIFICATION_SUCCESS(HttpStatus.OK, "OWNER2001", "사장 인증 성공"), + ; + + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ReasonDto getReason() { + return ReasonDto.builder() + .isSuccess(true) + .message(message) + .code(code) + .build(); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return ReasonDto.builder() + .httpStatus(httpStatus) + .isSuccess(true) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/eatsfine/eatsfine/global/annotation/CurrentUser.java b/src/main/java/com/eatsfine/eatsfine/global/annotation/CurrentUser.java new file mode 100644 index 00000000..2304a3d0 --- /dev/null +++ b/src/main/java/com/eatsfine/eatsfine/global/annotation/CurrentUser.java @@ -0,0 +1,14 @@ +package com.eatsfine.eatsfine.global.annotation; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : #this") +public @interface CurrentUser { +} diff --git a/src/main/java/com/eatsfine/eatsfine/global/apiPayload/handler/GeneralExceptionAdvice.java b/src/main/java/com/eatsfine/eatsfine/global/apiPayload/handler/GeneralExceptionAdvice.java index 7b2212f4..34f9345c 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/apiPayload/handler/GeneralExceptionAdvice.java +++ b/src/main/java/com/eatsfine/eatsfine/global/apiPayload/handler/GeneralExceptionAdvice.java @@ -1,5 +1,6 @@ package com.eatsfine.eatsfine.global.apiPayload.handler; +import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import com.eatsfine.eatsfine.global.apiPayload.code.BaseErrorCode; import com.eatsfine.eatsfine.global.apiPayload.code.status.ErrorStatus; @@ -10,6 +11,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -56,6 +58,22 @@ public ResponseEntity validation(ConstraintViolationException e, WebRequ return handleExceptionInternalFalse(e, ErrorStatus._BAD_REQUEST, HttpHeaders.EMPTY, ErrorStatus._BAD_REQUEST.getHttpStatus(), request, errorMessage); } + // @PreAuthorize 권한 실패 시 발생하는 예외 캐치 + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDeniedException(AccessDeniedException e, WebRequest request) { + log.warn("Access Denied: {}", e.getMessage()); + + // 2. 기존 메서드를 활용해 ResponseEntity로 반환 + return handleExceptionInternalFalse( + e, + UserErrorStatus.FORBIDDEN_OWNER, + HttpHeaders.EMPTY, + UserErrorStatus.FORBIDDEN_OWNER.getHttpStatus(), + request, + null + ); + } + // 3. 커스텀 예외용 내부 응답 생성 메서드 private ResponseEntity handleExceptionInternal(Exception e, BaseErrorCode code, HttpHeaders headers, HttpServletRequest request) { // 정의하신 ApiResponse.onFailure(BaseErrorCode code, T result)를 호출합니다. diff --git a/src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java index c8da9390..c15046a6 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java @@ -1,10 +1,8 @@ package com.eatsfine.eatsfine.global.auth; - - - import com.eatsfine.eatsfine.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -28,7 +26,11 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep throw new UsernameNotFoundException("비밀번호 기반 로그인 대상이 아닙니다."); } - return new User(user.getEmail(), password, List.of()); + return new User( + user.getEmail(), + password, + List.of(new SimpleGrantedAuthority(user.getRole().name())) + ); } } diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java index a980f7ac..2c842982 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java @@ -14,6 +14,7 @@ import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @@ -26,19 +27,21 @@ import java.time.Duration; import java.util.List; + @Configuration @EnableWebSecurity @RequiredArgsConstructor +@EnableMethodSecurity public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CustomAuthenticationEntryPoint authenticationEntryPoint; private final CustomAccessDeniedHandler accessDeniedHandler; - private final CustomOAuth2MemberServiceImpl customOAuth2UserService; private final CustomOAuth2SuccessHandler customOAuth2SuccessHandler; private final CustomOAuth2FailureHandler customOAuth2FailureHandler; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -46,19 +49,18 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .exceptionHandling(exceptions -> exceptions .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler) ) - .authorizeHttpRequests(auth -> auth + // preflight은 항상 허용 .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() - .requestMatchers("/oauth2/**", "/login/oauth2/**").permitAll() - + // 공개 리소스 / 인증 없이 .requestMatchers( "/api/auth/**", + "/oauth2/**", "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", @@ -92,12 +94,12 @@ public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequest } @Bean - public CorsConfigurationSource corsConfigurationSource() { + public CorsConfigurationSource corsConfigurationSource() { // cors 설정 CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOriginPatterns(List.of("*")); + config.setAllowedOriginPatterns(List.of("*")); // 운영 환경에서는 정확한 도메인만 명시 config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); config.setAllowedHeaders(List.of("*")); - config.setExposedHeaders(List.of("Authorization", "Set-Cookie")); + config.setExposedHeaders(List.of("Authorization", "Set-Cookie")); //쿠키, Authorization 헤더 노출 config.setAllowCredentials(true); config.setMaxAge(Duration.ofHours(1)); @@ -110,4 +112,6 @@ public CorsConfigurationSource corsConfigurationSource() { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + } + diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java index 30258362..e55c34e7 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java @@ -1,5 +1,7 @@ package com.eatsfine.eatsfine.global.config.jwt; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -9,12 +11,15 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +@Slf4j @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { @@ -41,22 +46,34 @@ protected void doFilterInternal(HttpServletRequest request, return; } + log.debug("요청 URI: {}", uri); + String token = JwtTokenProvider.resolveToken(request); if (token != null && jwtTokenProvider.validateToken(token)) { try { - String email = jwtTokenProvider.getEmailFromToken(token); - UserDetails userDetails = userDetailsService.loadUserByUsername(email); + // uri.startsWith() 로직 삭제 + // 토큰이 없으면 아래 if문에서 알아서 걸러지고 다음 필터로 넘어감. + // -> SecurityConfig의 permitAll() 설정에 따라 통과 여부 결정 + + Authentication authentication = jwtTokenProvider.getAuthentication(token); + + if(authentication instanceof UsernamePasswordAuthenticationToken) { + ((UsernamePasswordAuthenticationToken) authentication) + .setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + } - UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken( - userDetails, null, userDetails.getAuthorities()); - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); - } catch (Exception e) { + } catch (ExpiredJwtException e) { // 예외 로그 출력 - System.out.println("JWT 인증 오류: " + e.getMessage()); + log.warn("만료된 JWT 토큰입니다. {}", e.getMessage()); + } catch (JwtException | IllegalArgumentException e) { + // JWT 관련 구조적 문제는 스텍트레이스 포함해서 기록 + log.warn("유효하지 않은 JWT 토큰입니다. {}", e.getMessage(), e); + } catch (Exception e) { + // 예상치 못한 시스템 에러는 error 레벨로 전체 기록 + log.error("JWT 인증 과정에서 예상치 못한 오류가 발생했습니다. {}", e.getMessage(), e); } } diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtTokenProvider.java b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtTokenProvider.java index f578c363..ad36e9e9 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtTokenProvider.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtTokenProvider.java @@ -1,10 +1,13 @@ package com.eatsfine.eatsfine.global.config.jwt; +import com.eatsfine.eatsfine.domain.user.exception.AuthException; import com.eatsfine.eatsfine.domain.user.exception.UserException; +import com.eatsfine.eatsfine.domain.user.status.AuthErrorStatus; import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; import com.eatsfine.eatsfine.global.config.properties.Constants; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; -import com.eatsfine.eatsfine.global.apiPayload.code.status.ErrorStatus; import com.eatsfine.eatsfine.global.config.properties.JwtProperties; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtException; @@ -22,9 +25,11 @@ import java.security.Key; import java.util.Collections; import java.util.Date; +import java.util.List; @Component @RequiredArgsConstructor +@Slf4j public class JwtTokenProvider { private final JwtProperties jwtProperties; @@ -36,11 +41,18 @@ private Key getSigningKey() { private final long accessTokenValidity = 1000L * 60 * 60; // 1시간 private final long refreshTokenValidity = 1000L * 60 * 60 * 24 * 7; // 7일 - public String createAccessToken(String email) { + public String createAccessToken(String email, String role) { + if (!StringUtils.hasText(role)) { + throw new AuthException(AuthErrorStatus.EMPTY_TOKEN_ROLE); + } + + Claims claims = Jwts.claims().setSubject(email); + claims.put("role", role); + Date now = new Date(); Date expiry = new Date(now.getTime() + accessTokenValidity); return Jwts.builder() - .setSubject(email) + .setClaims(claims) .setIssuedAt(now) .setExpiration(expiry) .signWith(getSigningKey(), SignatureAlgorithm.HS256) @@ -79,7 +91,17 @@ public Authentication getAuthentication(String token) { String email = claims.getSubject(); - User principal = new User(email, "", Collections.emptyList()); + + String role = claims.get("role", String.class); + + if (!StringUtils.hasText(role)) { + log.error("JWT Token does not contain role claim for user: {}", claims.getSubject()); + throw new AuthException(AuthErrorStatus.EMPTY_TOKEN_ROLE); + } + + List authorities = Collections.singletonList(new SimpleGrantedAuthority(role)); + + User principal = new User(email, "", authorities); return new UsernamePasswordAuthenticationToken(principal, token, principal.getAuthorities()); } @@ -95,7 +117,7 @@ public static String resolveToken(HttpServletRequest request) { public Authentication extractAuthentication(HttpServletRequest request) { String accessToken = resolveToken(request); if (accessToken == null || !validateToken(accessToken)) { - throw new UserException(UserErrorStatus.INVALID_TOKEN); + throw new AuthException(AuthErrorStatus.INVALID_TOKEN); } return getAuthentication(accessToken); } diff --git a/src/test/java/com/eatsfine/eatsfine/controller/HealthControllerTest.java b/src/test/java/com/eatsfine/eatsfine/controller/HealthControllerTest.java index eadabdd1..993e087a 100644 --- a/src/test/java/com/eatsfine/eatsfine/controller/HealthControllerTest.java +++ b/src/test/java/com/eatsfine/eatsfine/controller/HealthControllerTest.java @@ -1,22 +1,36 @@ package com.eatsfine.eatsfine.controller; +import com.eatsfine.eatsfine.global.auth.CustomAccessDeniedHandler; +import com.eatsfine.eatsfine.global.auth.CustomAuthenticationEntryPoint; import com.eatsfine.eatsfine.global.config.DeployProperties; +import com.eatsfine.eatsfine.global.config.jwt.JwtAuthenticationFilter; import com.eatsfine.eatsfine.global.controller.HealthController; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.security.test.context.support.WithMockUser; + @ActiveProfiles("test") @WebMvcTest(controllers = HealthController.class) @EnableConfigurationProperties(DeployProperties.class) @@ -27,7 +41,29 @@ class HealthControllerTest { @Autowired private MockMvc mockMvc; + @MockBean + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @MockBean + private CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + + @MockBean + private CustomAccessDeniedHandler customAccessDeniedHandler; + + @BeforeEach + void setUp() throws ServletException, IOException { + doAnswer(invocation -> { + HttpServletRequest request = invocation.getArgument(0); + HttpServletResponse response = invocation.getArgument(1); + FilterChain chain = invocation.getArgument(2); + chain.doFilter(request, response); + return null; + }).when(jwtAuthenticationFilter).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class), + any(FilterChain.class)); + } + @Test + @WithMockUser @DisplayName("healthCheck : 현재 서버가 살아있다면 200 상태코드와 함께 활성화된 프로필을 문자열로 응답한다") void healthCheckTest() throws Exception { mockMvc diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 35b32f4f..8e1b49f0 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -1,3 +1,6 @@ +server: + profile: test + spring: datasource: url: jdbc:h2:mem:testdb;MODE=MySQL @@ -13,6 +16,9 @@ spring: format_sql: true dialect: org.hibernate.dialect.H2Dialect +jwt: + secret: test_secret_key_for_jwt_properties_validation_must_be_long_enough + payment: toss: widget-secret-key: test_sk_sample_key_for_testing From c7cddecda70a46ac431bb68abaa72684e20d4ae9 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sat, 7 Feb 2026 04:26:19 +0900 Subject: [PATCH 59/83] =?UTF-8?q?[Fix]=20=EC=95=BD=EA=B4=80=EB=8F=99?= =?UTF-8?q?=EC=9D=98=20=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/eatsfine/eatsfine/domain/term/entity/Term.java | 9 +++------ .../com/eatsfine/eatsfine/domain/user/entity/User.java | 10 ++++++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java b/src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java index cf0f4970..c2352400 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java @@ -3,10 +3,7 @@ import com.eatsfine.eatsfine.domain.user.entity.User; import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; @Entity @Getter @@ -19,7 +16,8 @@ public class Term extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true) + @Setter + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false, unique = true) private User user; @@ -31,5 +29,4 @@ public class Term extends BaseEntity { @Column(name = "marketing_consent", nullable = false) private Boolean marketingConsent; - } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java index fb7a2cbb..028342cc 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java @@ -1,5 +1,6 @@ package com.eatsfine.eatsfine.domain.user.entity; +import com.eatsfine.eatsfine.domain.term.entity.Term; import com.eatsfine.eatsfine.domain.user.enums.Role; import com.eatsfine.eatsfine.domain.user.enums.SocialType; import com.eatsfine.eatsfine.global.common.BaseEntity; @@ -70,9 +71,18 @@ public void updateRefreshToken(String refreshToken){ this.refreshToken = refreshToken; } + public void setTerm(Term term) { + this.term = term; + if (term != null) term.setUser(this); + } + public void linkSocial(SocialType socialType, String socialId) { this.socialType = socialType; this.socialId = socialId; } + @OneToOne(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true) + private Term term; + + } \ No newline at end of file From aef94e9ba6a25541c42577af0c2e265c5442867c Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sat, 7 Feb 2026 04:28:02 +0900 Subject: [PATCH 60/83] =?UTF-8?q?[Fix]=20=ED=86=A0=ED=81=B0=20=EC=9E=AC?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EB=B0=9B=EC=9D=84=EB=95=8C,=20=EC=97=AD?= =?UTF-8?q?=ED=95=A0=EB=8F=84=20=EB=B0=9B=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/AuthController.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java index 5ac0c596..d2a73a4b 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java @@ -1,9 +1,14 @@ package com.eatsfine.eatsfine.domain.user.controller; import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; +import com.eatsfine.eatsfine.domain.user.entity.User; +import com.eatsfine.eatsfine.domain.user.exception.AuthException; +import com.eatsfine.eatsfine.domain.user.repository.UserRepository; import com.eatsfine.eatsfine.domain.user.service.authService.AuthTokenService; +import com.eatsfine.eatsfine.domain.user.status.AuthErrorStatus; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; +import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; @@ -27,6 +32,8 @@ public class AuthController { private final AuthTokenService authTokenService; private final AuthCookieProvider authCookieProvider; + private final JwtTokenProvider jwtTokenProvider; + private final UserRepository userRepository; @PostMapping("/reissue") @Operation(summary = "재발급 API", description = "refreshToken을 재발급 하는 API입니다.") @@ -36,6 +43,18 @@ public ResponseEntity> reissue( ) { log.info("[REISSUE API] 재발급 요청 받음. refreshToken={}", refreshToken); + // refreshToken에서 email 추출 + if (refreshToken == null || refreshToken.isBlank()) { + throw new AuthException(AuthErrorStatus.REFRESH_TOKEN_MISSING); + } + + String email = jwtTokenProvider.getEmailFromToken(refreshToken); + + // DB에서 User 조회 + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new AuthException(AuthErrorStatus.INVALID_TOKEN)); + + // 토큰 재발급 AuthTokenService.ReissueResult result = authTokenService.reissue(refreshToken, user.getRole()); ResponseCookie refreshCookie = authCookieProvider.refreshTokenCookie(result.refreshToken()); From 30d23f3276db95c7a9b7f2d4209c01c094904a7a Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sat, 7 Feb 2026 04:30:24 +0900 Subject: [PATCH 61/83] =?UTF-8?q?[Chore]=20=EC=97=90=EB=9F=AC=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/apiPayload/handler/GeneralExceptionAdvice.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/global/apiPayload/handler/GeneralExceptionAdvice.java b/src/main/java/com/eatsfine/eatsfine/global/apiPayload/handler/GeneralExceptionAdvice.java index 34f9345c..f7ccd570 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/apiPayload/handler/GeneralExceptionAdvice.java +++ b/src/main/java/com/eatsfine/eatsfine/global/apiPayload/handler/GeneralExceptionAdvice.java @@ -1,6 +1,6 @@ package com.eatsfine.eatsfine.global.apiPayload.handler; -import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; +import com.eatsfine.eatsfine.domain.user.status.AuthErrorStatus; import com.eatsfine.eatsfine.global.apiPayload.ApiResponse; import com.eatsfine.eatsfine.global.apiPayload.code.BaseErrorCode; import com.eatsfine.eatsfine.global.apiPayload.code.status.ErrorStatus; @@ -66,9 +66,9 @@ public ResponseEntity handleAccessDeniedException(AccessDeniedException // 2. 기존 메서드를 활용해 ResponseEntity로 반환 return handleExceptionInternalFalse( e, - UserErrorStatus.FORBIDDEN_OWNER, + AuthErrorStatus.FORBIDDEN_OWNER, HttpHeaders.EMPTY, - UserErrorStatus.FORBIDDEN_OWNER.getHttpStatus(), + AuthErrorStatus.FORBIDDEN_OWNER.getHttpStatus(), request, null ); From c5de559ea5dd66255f58134033be5b362f94b8a3 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sat, 7 Feb 2026 05:17:29 +0900 Subject: [PATCH 62/83] =?UTF-8?q?=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EC=97=86=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../eatsfine/eatsfine/domain/user/status/UserErrorStatus.java | 2 -- .../com/eatsfine/eatsfine/controller/HealthControllerTest.java | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 23961013..9d6fe36d 100644 --- a/build.gradle +++ b/build.gradle @@ -90,4 +90,4 @@ tasks.named("bootJar"){ tasks.named('jar') { enabled = false -} +} \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java index a0c1544f..0a6d6c2d 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java @@ -13,8 +13,6 @@ public enum UserErrorStatus implements BaseErrorCode { // 멤버 관련 에러 MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."), - NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다."), - NAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "이름은 필수 입니다."), EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER4003", "이미 존재하는 이메일입니다."), diff --git a/src/test/java/com/eatsfine/eatsfine/controller/HealthControllerTest.java b/src/test/java/com/eatsfine/eatsfine/controller/HealthControllerTest.java index 993e087a..fc997345 100644 --- a/src/test/java/com/eatsfine/eatsfine/controller/HealthControllerTest.java +++ b/src/test/java/com/eatsfine/eatsfine/controller/HealthControllerTest.java @@ -75,4 +75,4 @@ void healthCheckTest() throws Exception { content().string("test") // active profile set to 'test' ); } -} +} \ No newline at end of file From f2f06c419054b1e88329f632aca1e206564d5afb Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sat, 7 Feb 2026 05:55:10 +0900 Subject: [PATCH 63/83] =?UTF-8?q?[Chore]=20=EC=86=8C=EC=85=9C=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=84=9C=EB=B2=84=20=EB=A6=AC=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EB=9E=99=ED=8A=B8=20url=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-local.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 97dcb9a8..7a62c3dc 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -33,16 +33,16 @@ spring: scope: - email - profile - redirect-uri: "https://eatsfine.co.kr/oauth/login/oauth2/code/google" + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" authorization-grant-type: authorization_code kakao: client-id: ${KAKAO_CLIENT_ID} - client-secret: "" + client-secret: ${KAKAO_CLIENT_SECRET} scope: - profile_nickname - profile_image - account_email - redirect-uri: "https://eatsfine.co.kr/oauth/code/kakao" + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" authorization-grant-type: authorization_code client-name: Kakao provider: kakao From c4d04b6a14567e62a9d9ff7599aa094da6e46887 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sat, 7 Feb 2026 06:48:15 +0900 Subject: [PATCH 64/83] =?UTF-8?q?[Refactor]=20=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=9E=98=EB=B9=97=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=ED=95=98=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 --- .../user/controller/AuthController.java | 10 +++++- .../domain/user/converter/UserConverter.java | 4 +-- .../eatsfine/domain/user/entity/User.java | 3 +- .../handler/CustomOAuth2SuccessHandler.java | 35 +++++++++---------- .../CustomOAuth2MemberServiceImpl.java | 8 ++++- .../oauthService/Oauth2MemberServiceImpl.java | 9 ++++- ...eOAuth2AuthorizationRequestRepository.java | 3 +- .../config/jwt/JwtAuthenticationFilter.java | 7 +--- 8 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java index d2a73a4b..df8060ba 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/AuthController.java @@ -48,7 +48,15 @@ public ResponseEntity> reissue( throw new AuthException(AuthErrorStatus.REFRESH_TOKEN_MISSING); } - String email = jwtTokenProvider.getEmailFromToken(refreshToken); + String email; + try { + if (!jwtTokenProvider.validateToken(refreshToken)) { + throw new AuthException(AuthErrorStatus.INVALID_TOKEN); + } + email = jwtTokenProvider.getEmailFromToken(refreshToken); + } catch (Exception e) { + throw new AuthException(AuthErrorStatus.INVALID_TOKEN); + } // DB에서 User 조회 User user = userRepository.findByEmail(email) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java index c8f9d4bc..f100d1a4 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java @@ -91,7 +91,7 @@ public static UserResponseDto.VerifyOwnerDto toVerifyOwnerResponse(User user) { /* 소셜 유저 생성 (최초 소셜 가입 등) - -소셜 로그인에서 email/nickname/phoneNumber 등을 확보한 후 엔티티 생성에 사용 + 소셜 로그인에서 email/nickname/profileImage 등을 확보한 후 엔티티 생성에 사용 */ public static User toSocialUser(String email, String nickName, String profileImage, String socialId, SocialType socialType) { @@ -101,7 +101,7 @@ public static User toSocialUser(String email, String nickName, String profileIma .profileImage(profileImage) .socialId(socialId) .socialType(socialType) - .phoneNumber("") + .phoneNumber(null) .role(ROLE_CUSTOMER) .build(); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java index bf7a3336..7f57960c 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java @@ -30,7 +30,6 @@ public class User extends BaseEntity { @Column(nullable = false, length = 20) private String phoneNumber; - @Getter @Enumerated(EnumType.STRING) private Role role; @@ -81,7 +80,7 @@ public void linkSocial(SocialType socialType, String socialId) { this.socialId = socialId; } - @OneToOne(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true) + @OneToOne(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) private Term term; } \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java index 16a4e9e9..a8e2a88e 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/exception/handler/CustomOAuth2SuccessHandler.java @@ -5,6 +5,8 @@ import com.eatsfine.eatsfine.domain.user.repository.UserRepository; import com.eatsfine.eatsfine.domain.user.status.AuthErrorStatus; import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; +import com.eatsfine.eatsfine.global.apiPayload.code.BaseErrorCode; +import com.eatsfine.eatsfine.global.apiPayload.code.ErrorReasonDto; import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; import jakarta.servlet.http.HttpServletRequest; @@ -106,27 +108,14 @@ public void onAuthenticationSuccess(HttpServletRequest request, response.sendRedirect(redirectUrl); } - private void redirectFail(HttpServletResponse response, AuthErrorStatus errorStatus) throws IOException { + private void redirectFail(HttpServletResponse response, BaseErrorCode errorStatus) throws IOException { + ErrorReasonDto reason = errorStatus.getReason(); String failUrl = UriComponentsBuilder.fromUriString(LOGIN_ERROR_REDIRECT_BASE) - .queryParam("error", errorStatus.getCode()) - .queryParam("message", errorStatus.getMessage()) + .queryParam("error", reason.getCode()) + .queryParam("message", reason.getMessage()) .build() .toUriString(); - - log.warn("[OAuth2 FAIL] errorCode={}, message={}, failUrl={}", - errorStatus.getCode(), errorStatus.getMessage(), failUrl); - response.sendRedirect(failUrl); - } - - private void redirectFail(HttpServletResponse response, UserErrorStatus errorStatus) throws IOException { - String failUrl = UriComponentsBuilder.fromUriString(LOGIN_ERROR_REDIRECT_BASE) - .queryParam("error", errorStatus.getCode()) - .queryParam("message", errorStatus.getMessage()) - .build() - .toUriString(); - - log.warn("[OAuth2 FAIL] errorCode={}, message={}, failUrl={}", - errorStatus.getCode(), errorStatus.getMessage(), failUrl); + log.warn("[OAuth2 FAIL] errorCode={}, message={}", reason.getCode(), reason.getMessage()); response.sendRedirect(failUrl); } @@ -169,7 +158,7 @@ private String extractSocialId(SocialType socialType, Map attrib private void logKakaoAccountStatus(Map attributes) { Object kakaoAccountObj = attributes.get("kakao_account"); if (!(kakaoAccountObj instanceof Map kakaoAccount)) { - log.warn("[KAKAO] kakao_account missing. attributes={}", attributes); + log.warn("[KAKAO] \n" + "카카오 계정이 없거나 유효하지 않습니다.. attributes={}", attributes); return; } @@ -180,5 +169,13 @@ private void logKakaoAccountStatus(Map attributes) { log.warn("[KAKAO] has_email={}, email_needs_agreement={}, is_email_valid={}, is_email_verified={}", hasEmail, emailNeedsAgreement, isEmailValid, isEmailVerified); + + + Object profileObj = kakaoAccount.get("profile"); + if (profileObj instanceof Map profile) { + Object nickname = profile.get("nickname"); + log.warn("[KAKAO] profile.nickname={}", nickname); + } } + } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/CustomOAuth2MemberServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/CustomOAuth2MemberServiceImpl.java index fb677f65..4f50597d 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/CustomOAuth2MemberServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/CustomOAuth2MemberServiceImpl.java @@ -28,7 +28,13 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic String email = null; String name = null; - SocialType socialType = SocialType.valueOf(provider.toUpperCase()); + SocialType socialType; + try { + socialType = SocialType.valueOf(provider.toUpperCase()); + } catch (IllegalArgumentException e) { + OAuth2Error error = new OAuth2Error("unsupported_provider", "지원되지 않는 소셜 로그인 제공자입니다: " + provider, null); + throw new OAuth2AuthenticationException(error, e); + } try { if (socialType == SocialType.GOOGLE) { diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberServiceImpl.java index 1efb583a..fff0a0f0 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/oauthService/Oauth2MemberServiceImpl.java @@ -4,7 +4,9 @@ import com.eatsfine.eatsfine.domain.user.converter.UserConverter; import com.eatsfine.eatsfine.domain.user.entity.User; import com.eatsfine.eatsfine.domain.user.enums.SocialType; +import com.eatsfine.eatsfine.domain.user.exception.AuthException; import com.eatsfine.eatsfine.domain.user.repository.UserRepository; +import com.eatsfine.eatsfine.domain.user.status.AuthErrorStatus; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -22,7 +24,12 @@ public class Oauth2MemberServiceImpl implements Oauth2MemberService { @Override public User findOrCreateOauthUser(SocialType socialType, String socialId, String email, String nickName) { - // 1. 소셜 ID로 이미 가입된 회원이 있는지 조회 + + if (email == null || email.isBlank()) { + throw new AuthException(AuthErrorStatus.OAUTH2_EMAIL_NOT_FOUND); + } + + // 소셜 ID로 이미 가입된 회원이 있는지 조회 return userRepository.findBySocialTypeAndSocialId(socialType, socialId) .orElseGet(() -> findByEmailOrJoin(socialType, socialId, email, nickName)); } diff --git a/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java b/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java index 4481ce09..6f6def11 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java @@ -24,7 +24,7 @@ public class HttpCookieOAuth2AuthorizationRequestRepository public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { log.info("[LOAD] 쿠키에서 AuthorizationRequest 로드 시도"); - // ⭐ 모든 쿠키 이름 출력 + // 모든 쿠키 이름 출력 if (request.getCookies() != null) { for (Cookie c : request.getCookies()) { log.info("[LOAD] 발견된 쿠키: name={}, value={}, path={}, domain={}", @@ -79,6 +79,7 @@ public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationReq // 환경에 따라 설정 cookie.setSecure(true); // 로컬 HTTP: false, 운영 HTTPS: true + cookie.setAttribute("SameSite", "Lax"); response.addCookie(cookie); log.info("[SAVE] AuthorizationRequest 쿠키 저장 완료 - name={}, path={}, maxAge={}, secure={}, domain={}", diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java index 871bd7e5..904267b6 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java @@ -9,7 +9,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetailsService; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; @@ -24,7 +23,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; - private final UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, @@ -33,7 +31,7 @@ protected void doFilterInternal(HttpServletRequest request, throws ServletException, IOException { String uri = request.getRequestURI(); - System.out.println("요청 URI: " + uri); + log.debug("요청 URI: {}", uri); // 인증 없이 통과시킬 경로들 if (uri.startsWith("/api/auth/login") || @@ -45,9 +43,6 @@ protected void doFilterInternal(HttpServletRequest request, return; } - log.debug("요청 URI: {}", uri); - - String token = JwtTokenProvider.resolveToken(request); if (token != null && jwtTokenProvider.validateToken(token)) { From a2c8b29c687e5e29673613afe34b57cd09cc7132 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sat, 7 Feb 2026 13:15:38 +0900 Subject: [PATCH 65/83] =?UTF-8?q?[Refactor]=20=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=9E=98=EB=B9=97=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=ED=95=98=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 --- .../java/com/eatsfine/eatsfine/domain/user/entity/User.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java index 7f57960c..7058c226 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java @@ -27,7 +27,7 @@ public class User extends BaseEntity { private String password; - @Column(nullable = false, length = 20) + @Column(nullable = true, length = 20) private String phoneNumber; @Enumerated(EnumType.STRING) From 888168bbe53e1df7b3f8039aa179a94d2594727b Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sat, 7 Feb 2026 13:18:16 +0900 Subject: [PATCH 66/83] =?UTF-8?q?[Refactor]=20=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=9E=98=EB=B9=97=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=ED=95=98=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 --- .../com/eatsfine/eatsfine/domain/user/entity/User.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java index 7058c226..19e21f9c 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java @@ -71,8 +71,13 @@ public void updateRefreshToken(String refreshToken){ } public void setTerm(Term term) { + if (this.term != null && this.term != term) { + this.term.setUser(null); + } this.term = term; - if (term != null) term.setUser(this); + if (term != null) { + term.setUser(this); + } } public void linkSocial(SocialType socialType, String socialId) { From cc5e0c4d347854f29457cb1ef4919e46c6d10cbb Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sat, 7 Feb 2026 23:50:48 +0900 Subject: [PATCH 67/83] =?UTF-8?q?[Feat]=20=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/eatsfine/eatsfine/domain/user/entity/User.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java index 19e21f9c..90aa33a3 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java @@ -70,6 +70,8 @@ public void updateRefreshToken(String refreshToken){ this.refreshToken = refreshToken; } + public void updatePassword(String password){this.password = password;} + public void setTerm(Term term) { if (this.term != null && this.term != term) { this.term.setUser(null); From d0324c1789a32612270d56d898c7d634d146bbbf Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sat, 7 Feb 2026 23:51:39 +0900 Subject: [PATCH 68/83] =?UTF-8?q?[Feat]=20=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 18 +++++++++++++-- .../user/service/userService/UserService.java | 2 ++ .../service/userService/UserServiceImpl.java | 22 +++++++++++++++++++ .../domain/user/status/UserErrorStatus.java | 4 +++- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java index af0ceeea..87d56c9e 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java @@ -78,8 +78,8 @@ public ApiResponse getMyInfo(HttpServletRequest req @PatchMapping(value = "/api/v1/member/info") @Operation( - summary = "닉네임/전화번호 수정 API - 인증 필요", - description = "닉네임/전화번호만 수정합니다. (JSON)", + summary = "이름/전화번호 수정 API - 인증 필요", + description = "이름/전화번호/이메일만 수정합니다. (JSON)", security = {@SecurityRequirement(name = "JWT")} ) public ResponseEntity> updateMyInfoText( @@ -151,4 +151,18 @@ public ResponseEntity> logout(HttpServletRequest request) { .body(ApiResponse.onSuccess("로그아웃이 되었습니다.")); } + @PutMapping("/api/v1/member/password") + @Operation( + summary = "비밀번호 변경 API - 인증 필요", + description = "비밀번호 변경하는 API입니다.", + security = {@SecurityRequirement(name = "JWT")} + ) + public ResponseEntity> changePassword + (@RequestBody @Valid UserRequestDto.ChangePasswordDto changePassword, HttpServletRequest request){ + + UserResponseDto.UpdatePasswordDto result = userService.changePassword(changePassword, request); + + return ResponseEntity.ok(ApiResponse.onSuccess(result)); + } + } \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java index 665b8a1f..8a97de67 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java @@ -23,4 +23,6 @@ public interface UserService { UserResponseDto.VerifyOwnerDto verifyOwner(UserRequestDto.VerifyOwnerDto verifyOwnerDto, HttpServletRequest request); + UserResponseDto.UpdatePasswordDto changePassword(UserRequestDto.ChangePasswordDto changePassword, HttpServletRequest request); + } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java index bad3f3ae..763d33b7 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java @@ -16,6 +16,7 @@ import com.eatsfine.eatsfine.domain.user.service.userService.UserService; import com.eatsfine.eatsfine.domain.user.status.AuthErrorStatus; import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; +import com.eatsfine.eatsfine.global.apiPayload.exception.GeneralException; import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; import com.eatsfine.eatsfine.global.s3.S3Service; import jakarta.servlet.http.HttpServletRequest; @@ -28,6 +29,8 @@ import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.web.multipart.MultipartFile; +import java.time.LocalDateTime; + @Slf4j @Service @RequiredArgsConstructor @@ -246,4 +249,23 @@ public UserResponseDto.VerifyOwnerDto verifyOwner(UserRequestDto.VerifyOwnerDto log.info("[OwnerAuth] 인증 성공 - 유저 권한이 OWNER로 변경되었습니다. 유저ID: {}", savedUser.getId()); return UserConverter.toVerifyOwnerResponse(savedUser); } + + @Override + @Transactional + public UserResponseDto.UpdatePasswordDto changePassword(UserRequestDto.ChangePasswordDto requestDto, HttpServletRequest request) { + + User user = getCurrentUser(request); + + // 현재 비밀번호 일치 여부 확인 + if (!passwordEncoder.matches(requestDto.getCurrentPassword(), user.getPassword())) { + throw new GeneralException(UserErrorStatus.PASSWORD_NOT_MATCH); + } + + // 새 비밀번호 암호화 및 업데이트 + String encryptedPassword = passwordEncoder.encode(requestDto.getNewPassword()); + user.updatePassword(encryptedPassword); + + // 결과 반환 + return UserConverter.toUpdatePasswordResponse(true, LocalDateTime.now(), "비밀번호가 성공적으로 변경되었습니다." ); + } } \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java index eac1df36..bb6fd5b3 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java @@ -16,7 +16,9 @@ public enum UserErrorStatus implements BaseErrorCode { NAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "이름은 필수 입니다."), EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER4003", "이미 존재하는 이메일입니다."), - INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4004", "비밀번호가 올바르지 않습니다."); + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4004", "비밀번호가 올바르지 않습니다."), + PASSWORD_NOT_MATCH(HttpStatus.BAD_REQUEST, "MEMBER4005", "현재 비밀번호가 일치하지 않습니다.") + ; private final HttpStatus httpStatus; private final String code; From 29b6b0f43c1bc722d86420fa03a29534899019c3 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sat, 7 Feb 2026 23:52:35 +0900 Subject: [PATCH 69/83] =?UTF-8?q?[Refactor]=20=EB=B3=B4=EC=95=88=EC=83=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95=EC=97=90=EC=84=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=EC=9D=80=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/eatsfine/domain/user/converter/UserConverter.java | 1 - .../eatsfine/domain/user/dto/request/UserRequestDto.java | 2 +- .../eatsfine/domain/user/dto/response/UserResponseDto.java | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java index f100d1a4..d99117ff 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java @@ -45,7 +45,6 @@ public static UserResponseDto.UserInfoDto toUserInfo(User user) { public static UserResponseDto.UpdateResponseDto toUpdateResponse(User user) { return UserResponseDto.UpdateResponseDto.builder() .profileImage(user.getProfileImage()) - .email(user.getEmail()) .name(user.getName()) .phoneNumber(user.getPhoneNumber()) .build(); diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java index a70cda77..2441d89f 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java @@ -61,8 +61,8 @@ public static class LoginDto { @Getter @Setter public static class UpdateDto { - private String email; private String name; + @Schema(description = "전화번호", nullable = true, defaultValue = "") private String phoneNumber; } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java index 88733622..28186ef1 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java @@ -43,7 +43,6 @@ public static class UserInfoDto{ @Builder public static class UpdateResponseDto{ private String profileImage; - private String email; private String name; private String phoneNumber; } From c293cba6e705420c6d4e21c5e5d0377a593301ef Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sun, 8 Feb 2026 22:43:31 +0900 Subject: [PATCH 70/83] =?UTF-8?q?[Refactor]=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=EC=98=88=EC=99=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EA=B2=BD=EB=A1=9C=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F?= =?UTF-8?q?=20=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B3=84?= =?UTF-8?q?=EC=A0=95=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/user/controller/UserController.java | 2 +- .../domain/user/service/userService/UserServiceImpl.java | 7 ++++++- .../eatsfine/domain/user/status/AuthErrorStatus.java | 3 +++ .../eatsfine/domain/user/status/UserErrorStatus.java | 2 -- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java index 87d56c9e..93021f28 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java @@ -79,7 +79,7 @@ public ApiResponse getMyInfo(HttpServletRequest req @PatchMapping(value = "/api/v1/member/info") @Operation( summary = "이름/전화번호 수정 API - 인증 필요", - description = "이름/전화번호/이메일만 수정합니다. (JSON)", + description = "이름/전화번호만 수정합니다. (JSON)", security = {@SecurityRequirement(name = "JWT")} ) public ResponseEntity> updateMyInfoText( diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java index 763d33b7..fee31069 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java @@ -256,9 +256,14 @@ public UserResponseDto.UpdatePasswordDto changePassword(UserRequestDto.ChangePas User user = getCurrentUser(request); + // 소셜 로그인 사용자 명시적 차단 + if (user.getPassword() == null || user.getPassword().isBlank()) { + throw new AuthException(AuthErrorStatus.OAUTH_PASSWORD_NOT_SUPPORTED); + } + // 현재 비밀번호 일치 여부 확인 if (!passwordEncoder.matches(requestDto.getCurrentPassword(), user.getPassword())) { - throw new GeneralException(UserErrorStatus.PASSWORD_NOT_MATCH); + throw new UserException(UserErrorStatus.PASSWORD_NOT_MATCH); } // 새 비밀번호 암호화 및 업데이트 diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java index f79c527d..c881a787 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java @@ -19,6 +19,9 @@ public enum AuthErrorStatus implements BaseErrorCode { EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH4004", "토큰이 만료되었습니다."), REFRESH_TOKEN_NOT_ISSUED(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH5001", "리프레시 토큰이 발급되지 않았습니다."), + //소설 로그인 유저 비번 수정 관련 에러 + OAUTH_PASSWORD_NOT_SUPPORTED(HttpStatus.CONFLICT, "AUTH_410", "소셜 로그인 계정은 비밀번호 변경을 지원하지 않습니다."), + EMPTY_TOKEN_ROLE(HttpStatus.UNAUTHORIZED, "AUTH407", "토큰 내 권한 정보가 누락되었습니다."), // 사장 인증 관련 에러 ALREADY_OWNER(HttpStatus.CONFLICT, "OWNER409", "이미 사장 회원입니다."), diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java index bb6fd5b3..418d6a73 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java @@ -12,9 +12,7 @@ public enum UserErrorStatus implements BaseErrorCode { // 멤버 관련 에러 MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."), - NAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "이름은 필수 입니다."), - EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER4003", "이미 존재하는 이메일입니다."), INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4004", "비밀번호가 올바르지 않습니다."), PASSWORD_NOT_MATCH(HttpStatus.BAD_REQUEST, "MEMBER4005", "현재 비밀번호가 일치하지 않습니다.") From 12ad54c5bd8bb232645025378ffa7f1bda735c14 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sun, 8 Feb 2026 22:51:43 +0900 Subject: [PATCH 71/83] =?UTF-8?q?[Fix]=20=EA=B4=84=ED=98=B8=20=EC=83=9D?= =?UTF-8?q?=EB=9E=B5=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/domain/user/entity/User.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java index 58f99d78..34cb864a 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java @@ -80,14 +80,13 @@ public void setTerm(Term term) { if (term != null) { term.setUser(this); } - - - public void linkSocial(SocialType socialType, String socialId) { - this.socialType = socialType; - this.socialId = socialId; } - @OneToOne(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) - private Term term; + public void linkSocial (SocialType socialType, String socialId){ + this.socialType = socialType; + this.socialId = socialId; + } -} \ No newline at end of file + @OneToOne(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) + private Term term; +} From 189c19b94eb46823a83591b548054a3202bc8140 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sun, 8 Feb 2026 22:52:14 +0900 Subject: [PATCH 72/83] =?UTF-8?q?[Fix]=20=EA=B4=84=ED=98=B8=20=EC=83=9D?= =?UTF-8?q?=EB=9E=B5=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/eatsfine/domain/user/entity/User.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java index 34cb864a..bd2fc6e3 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java @@ -82,11 +82,11 @@ public void setTerm(Term term) { } } - public void linkSocial (SocialType socialType, String socialId){ - this.socialType = socialType; - this.socialId = socialId; - } + public void linkSocial (SocialType socialType, String socialId){ + this.socialType = socialType; + this.socialId = socialId; + } - @OneToOne(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) - private Term term; + @OneToOne(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) + private Term term; } From 80a4cb7b34dd34029583f30d926ff95442adfcf1 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sun, 8 Feb 2026 23:06:38 +0900 Subject: [PATCH 73/83] =?UTF-8?q?[Fix]=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=ED=9B=84=20refresh=20token=20?= =?UTF-8?q?=EB=AC=B4=ED=9A=A8=ED=99=94=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/controller/UserController.java | 6 ++++-- .../user/service/userService/UserService.java | 3 ++- .../service/userService/UserServiceImpl.java | 16 +++++++++++++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java index 93021f28..aee6c6b9 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java @@ -12,6 +12,7 @@ import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestBody; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -158,9 +159,10 @@ public ResponseEntity> logout(HttpServletRequest request) { security = {@SecurityRequirement(name = "JWT")} ) public ResponseEntity> changePassword - (@RequestBody @Valid UserRequestDto.ChangePasswordDto changePassword, HttpServletRequest request){ + (@RequestBody @Valid UserRequestDto.ChangePasswordDto changePassword, + HttpServletRequest request, HttpServletResponse response){ - UserResponseDto.UpdatePasswordDto result = userService.changePassword(changePassword, request); + UserResponseDto.UpdatePasswordDto result = userService.changePassword(changePassword, request, response); return ResponseEntity.ok(ApiResponse.onSuccess(result)); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java index 8a97de67..bc562799 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java @@ -3,6 +3,7 @@ import com.eatsfine.eatsfine.domain.user.dto.request.UserRequestDto; import com.eatsfine.eatsfine.domain.user.dto.response.UserResponseDto; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -23,6 +24,6 @@ public interface UserService { UserResponseDto.VerifyOwnerDto verifyOwner(UserRequestDto.VerifyOwnerDto verifyOwnerDto, HttpServletRequest request); - UserResponseDto.UpdatePasswordDto changePassword(UserRequestDto.ChangePasswordDto changePassword, HttpServletRequest request); + UserResponseDto.UpdatePasswordDto changePassword(UserRequestDto.ChangePasswordDto changePassword, HttpServletRequest request, HttpServletResponse response); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java index c4c7667a..92445491 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java @@ -13,15 +13,17 @@ import com.eatsfine.eatsfine.domain.user.exception.AuthException; import com.eatsfine.eatsfine.domain.user.exception.UserException; import com.eatsfine.eatsfine.domain.user.repository.UserRepository; -import com.eatsfine.eatsfine.domain.user.service.userService.UserService; import com.eatsfine.eatsfine.domain.user.status.AuthErrorStatus; import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; -import com.eatsfine.eatsfine.global.apiPayload.exception.GeneralException; +import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; import com.eatsfine.eatsfine.global.s3.S3Service; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -39,6 +41,7 @@ public class UserServiceImpl implements UserService { private final TermRepository termRepository; private final PasswordEncoder passwordEncoder; private final JwtTokenProvider jwtTokenProvider; + private final AuthCookieProvider authCookieProvider; private final S3Service s3Service; private final BusinessNumberValidator businessNumberValidator; @@ -252,7 +255,10 @@ public UserResponseDto.VerifyOwnerDto verifyOwner(UserRequestDto.VerifyOwnerDto @Override @Transactional - public UserResponseDto.UpdatePasswordDto changePassword(UserRequestDto.ChangePasswordDto requestDto, HttpServletRequest request) { + public UserResponseDto.UpdatePasswordDto changePassword( + UserRequestDto.ChangePasswordDto requestDto, + HttpServletRequest request, + HttpServletResponse response) { User user = getCurrentUser(request); @@ -270,6 +276,10 @@ public UserResponseDto.UpdatePasswordDto changePassword(UserRequestDto.ChangePas String encryptedPassword = passwordEncoder.encode(requestDto.getNewPassword()); user.updatePassword(encryptedPassword); + user.updateRefreshToken(null); + ResponseCookie clearCookie = authCookieProvider.clearRefreshTokenCookie(); + response.addHeader(HttpHeaders.SET_COOKIE, clearCookie.toString()); + // 결과 반환 return UserConverter.toUpdatePasswordResponse(true, LocalDateTime.now(), "비밀번호가 성공적으로 변경되었습니다." ); } From a0831d05caee24bf5d5d08b693ba79fc612f5672 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sun, 8 Feb 2026 23:07:50 +0900 Subject: [PATCH 74/83] =?UTF-8?q?[Chore]=20=EC=97=90=EB=9F=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=BB=A8=EB=B2=A4?= =?UTF-8?q?=EC=85=98=20=EB=B6=88=EC=9D=BC=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java index c881a787..6fec05c6 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java @@ -19,8 +19,8 @@ public enum AuthErrorStatus implements BaseErrorCode { EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH4004", "토큰이 만료되었습니다."), REFRESH_TOKEN_NOT_ISSUED(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH5001", "리프레시 토큰이 발급되지 않았습니다."), - //소설 로그인 유저 비번 수정 관련 에러 - OAUTH_PASSWORD_NOT_SUPPORTED(HttpStatus.CONFLICT, "AUTH_410", "소셜 로그인 계정은 비밀번호 변경을 지원하지 않습니다."), + //소셜 로그인 유저 비번 수정 관련 에러 + OAUTH_PASSWORD_NOT_SUPPORTED(HttpStatus.CONFLICT, "AUTH410", "소셜 로그인 계정은 비밀번호 변경을 지원하지 않습니다."), EMPTY_TOKEN_ROLE(HttpStatus.UNAUTHORIZED, "AUTH407", "토큰 내 권한 정보가 누락되었습니다."), // 사장 인증 관련 에러 From 3f5f13196c1fd2feabcfe1cb08ac6625768c3392 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Sun, 8 Feb 2026 23:19:33 +0900 Subject: [PATCH 75/83] =?UTF-8?q?[Refactor]=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20=EC=BF=A0=ED=82=A4?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/controller/UserController.java | 16 ++++++++++------ .../user/service/userService/UserService.java | 2 +- .../service/userService/UserServiceImpl.java | 10 +--------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java index aee6c6b9..c4f22889 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java @@ -12,7 +12,6 @@ import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestBody; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -158,13 +157,18 @@ public ResponseEntity> logout(HttpServletRequest request) { description = "비밀번호 변경하는 API입니다.", security = {@SecurityRequirement(name = "JWT")} ) - public ResponseEntity> changePassword - (@RequestBody @Valid UserRequestDto.ChangePasswordDto changePassword, - HttpServletRequest request, HttpServletResponse response){ + public ResponseEntity> changePassword( + @RequestBody @Valid UserRequestDto.ChangePasswordDto changePassword, HttpServletRequest request + ) { + UserResponseDto.UpdatePasswordDto result = userService.changePassword(changePassword, request); - UserResponseDto.UpdatePasswordDto result = userService.changePassword(changePassword, request, response); + // 비밀번호 변경 성공 시 refreshToken 쿠키 삭제 + ResponseCookie clearCookie = authCookieProvider.clearRefreshTokenCookie(); - return ResponseEntity.ok(ApiResponse.onSuccess(result)); + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, clearCookie.toString()) + .body(ApiResponse.onSuccess(result)); } + } \ No newline at end of file diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java index bc562799..60c1f88b 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserService.java @@ -24,6 +24,6 @@ public interface UserService { UserResponseDto.VerifyOwnerDto verifyOwner(UserRequestDto.VerifyOwnerDto verifyOwnerDto, HttpServletRequest request); - UserResponseDto.UpdatePasswordDto changePassword(UserRequestDto.ChangePasswordDto changePassword, HttpServletRequest request, HttpServletResponse response); + UserResponseDto.UpdatePasswordDto changePassword(UserRequestDto.ChangePasswordDto changePassword, HttpServletRequest request); } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java index 92445491..85a13a4b 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java @@ -15,15 +15,11 @@ import com.eatsfine.eatsfine.domain.user.repository.UserRepository; import com.eatsfine.eatsfine.domain.user.status.AuthErrorStatus; import com.eatsfine.eatsfine.domain.user.status.UserErrorStatus; -import com.eatsfine.eatsfine.global.auth.AuthCookieProvider; import com.eatsfine.eatsfine.global.config.jwt.JwtTokenProvider; import com.eatsfine.eatsfine.global.s3.S3Service; import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseCookie; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -41,7 +37,6 @@ public class UserServiceImpl implements UserService { private final TermRepository termRepository; private final PasswordEncoder passwordEncoder; private final JwtTokenProvider jwtTokenProvider; - private final AuthCookieProvider authCookieProvider; private final S3Service s3Service; private final BusinessNumberValidator businessNumberValidator; @@ -257,8 +252,7 @@ public UserResponseDto.VerifyOwnerDto verifyOwner(UserRequestDto.VerifyOwnerDto @Transactional public UserResponseDto.UpdatePasswordDto changePassword( UserRequestDto.ChangePasswordDto requestDto, - HttpServletRequest request, - HttpServletResponse response) { + HttpServletRequest request) { User user = getCurrentUser(request); @@ -277,8 +271,6 @@ public UserResponseDto.UpdatePasswordDto changePassword( user.updatePassword(encryptedPassword); user.updateRefreshToken(null); - ResponseCookie clearCookie = authCookieProvider.clearRefreshTokenCookie(); - response.addHeader(HttpHeaders.SET_COOKIE, clearCookie.toString()); // 결과 반환 return UserConverter.toUpdatePasswordResponse(true, LocalDateTime.now(), "비밀번호가 성공적으로 변경되었습니다." ); From 08949febddbb074d0bd5faedaf3df6ab96a37566 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Mon, 9 Feb 2026 01:29:48 +0900 Subject: [PATCH 76/83] =?UTF-8?q?[Refactor]=20setSecure=20=EB=8F=99?= =?UTF-8?q?=EC=A0=81=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...pCookieOAuth2AuthorizationRequestRepository.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java b/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java index 6f6def11..0770beea 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java @@ -64,7 +64,7 @@ public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationReq HttpServletResponse response) { if (authorizationRequest == null) { log.info("[SAVE] authorizationRequest가 null이므로 쿠키 제거"); - removeAuthorizationRequestCookies(response); + removeAuthorizationRequestCookies(request, response); return; } @@ -78,7 +78,7 @@ public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationReq cookie.setMaxAge(COOKIE_EXPIRE_SECONDS); // 환경에 따라 설정 - cookie.setSecure(true); // 로컬 HTTP: false, 운영 HTTPS: true + cookie.setSecure(request.isSecure()); cookie.setAttribute("SameSite", "Lax"); response.addCookie(cookie); @@ -95,19 +95,20 @@ public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest HttpServletResponse response) { log.info("[REMOVE] AuthorizationRequest 제거"); OAuth2AuthorizationRequest authRequest = loadAuthorizationRequest(request); - removeAuthorizationRequestCookies(response); + removeAuthorizationRequestCookies(request, response); return authRequest; } - private void removeAuthorizationRequestCookies(HttpServletResponse response) { + private void removeAuthorizationRequestCookies(HttpServletRequest request, + HttpServletResponse response) { Cookie cookie = new Cookie(OAUTH2_AUTH_REQUEST_COOKIE_NAME, ""); cookie.setPath("/"); cookie.setHttpOnly(true); cookie.setMaxAge(0); - cookie.setSecure(true); // 저장할 때와 동일하게 + cookie.setSecure(request.isSecure()); // 저장할 때와 동일하게 cookie.setAttribute("SameSite", "Lax"); // 저장할 때와 동일하게 response.addCookie(cookie); - log.info("[REMOVE] 쿠키 제거 완료"); + log.info("[REMOVE] 쿠키 제거 완료 - secure={}", cookie.getSecure()); } } \ No newline at end of file From d9fc2eb638bfcd8ae9c82e971479d870cd7b0d0f Mon Sep 17 00:00:00 2001 From: SungMinju Date: Mon, 9 Feb 2026 01:38:50 +0900 Subject: [PATCH 77/83] =?UTF-8?q?[Refactor]=20=EB=82=98=EB=A8=B8=EC=A7=80?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=9B=90=EC=83=81=EB=B3=B5=EA=B7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/status/AuthErrorStatus.java | 2 +- ...pCookieOAuth2AuthorizationRequestRepository.java | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java index 6fec05c6..32b6790b 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/AuthErrorStatus.java @@ -20,7 +20,7 @@ public enum AuthErrorStatus implements BaseErrorCode { REFRESH_TOKEN_NOT_ISSUED(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH5001", "리프레시 토큰이 발급되지 않았습니다."), //소셜 로그인 유저 비번 수정 관련 에러 - OAUTH_PASSWORD_NOT_SUPPORTED(HttpStatus.CONFLICT, "AUTH410", "소셜 로그인 계정은 비밀번호 변경을 지원하지 않습니다."), + OAUTH_PASSWORD_NOT_SUPPORTED(HttpStatus.CONFLICT, "AUTH4010", "소셜 로그인 계정은 비밀번호 변경을 지원하지 않습니다."), EMPTY_TOKEN_ROLE(HttpStatus.UNAUTHORIZED, "AUTH407", "토큰 내 권한 정보가 누락되었습니다."), // 사장 인증 관련 에러 diff --git a/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java b/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java index 0770beea..8b725a4a 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java +++ b/src/main/java/com/eatsfine/eatsfine/global/auth/HttpCookieOAuth2AuthorizationRequestRepository.java @@ -64,7 +64,7 @@ public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationReq HttpServletResponse response) { if (authorizationRequest == null) { log.info("[SAVE] authorizationRequest가 null이므로 쿠키 제거"); - removeAuthorizationRequestCookies(request, response); + removeAuthorizationRequestCookies(response); return; } @@ -78,7 +78,7 @@ public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationReq cookie.setMaxAge(COOKIE_EXPIRE_SECONDS); // 환경에 따라 설정 - cookie.setSecure(request.isSecure()); + cookie.setSecure(request.isSecure()); // 로컬 HTTP: false, 운영 HTTPS: true cookie.setAttribute("SameSite", "Lax"); response.addCookie(cookie); @@ -95,20 +95,19 @@ public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest HttpServletResponse response) { log.info("[REMOVE] AuthorizationRequest 제거"); OAuth2AuthorizationRequest authRequest = loadAuthorizationRequest(request); - removeAuthorizationRequestCookies(request, response); + removeAuthorizationRequestCookies(response); return authRequest; } - private void removeAuthorizationRequestCookies(HttpServletRequest request, - HttpServletResponse response) { + private void removeAuthorizationRequestCookies(HttpServletResponse response) { Cookie cookie = new Cookie(OAUTH2_AUTH_REQUEST_COOKIE_NAME, ""); cookie.setPath("/"); cookie.setHttpOnly(true); cookie.setMaxAge(0); - cookie.setSecure(request.isSecure()); // 저장할 때와 동일하게 + cookie.setSecure(true); // 저장할 때와 동일하게 cookie.setAttribute("SameSite", "Lax"); // 저장할 때와 동일하게 response.addCookie(cookie); - log.info("[REMOVE] 쿠키 제거 완료 - secure={}", cookie.getSecure()); + log.info("[REMOVE] 쿠키 제거 완료"); } } \ No newline at end of file From 9a878702fe40b7c80b9576e12070e7f13e5a2030 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Wed, 11 Feb 2026 18:22:23 +0900 Subject: [PATCH 78/83] =?UTF-8?q?[Fix]=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=98=A4=EB=A5=98=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 --- .../service/userService/UserServiceImpl.java | 21 ++++++++++++------- .../domain/user/status/UserErrorStatus.java | 3 ++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java index 85a13a4b..de37e2cf 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java @@ -104,14 +104,16 @@ public String updateMemberInfo(UserRequestDto.UpdateDto updateDto, boolean changed = false; // 이름/전화번호 부분 수정 - if (updateDto.getName() != null && !updateDto.getName().isBlank()) { - user.updateName(updateDto.getName()); - changed = true; - } + if (updateDto != null) { + if (updateDto.getName() != null && !updateDto.getName().isBlank()) { + user.updateName(updateDto.getName()); + changed = true; + } - if (updateDto.getPhoneNumber() != null && !updateDto.getPhoneNumber().isBlank()) { - user.updatePhoneNumber(updateDto.getPhoneNumber()); - changed = true; + if (updateDto.getPhoneNumber() != null && !updateDto.getPhoneNumber().isBlank()) { + user.updatePhoneNumber(updateDto.getPhoneNumber()); + changed = true; + } } //프로필 이미지 부분 수정 (파일이 들어온 경우에만) @@ -266,6 +268,11 @@ public UserResponseDto.UpdatePasswordDto changePassword( throw new UserException(UserErrorStatus.PASSWORD_NOT_MATCH); } + // 새 비밀번호가 현재 비밀번호와 동일한지 확인 + if (passwordEncoder.matches(requestDto.getNewPassword(), user.getPassword())) { + throw new UserException(UserErrorStatus.SAME_PASSWORD); + } + // 새 비밀번호 암호화 및 업데이트 String encryptedPassword = passwordEncoder.encode(requestDto.getNewPassword()); user.updatePassword(encryptedPassword); diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java index 418d6a73..db3ceb72 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java @@ -15,7 +15,8 @@ public enum UserErrorStatus implements BaseErrorCode { NAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "이름은 필수 입니다."), EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER4003", "이미 존재하는 이메일입니다."), INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4004", "비밀번호가 올바르지 않습니다."), - PASSWORD_NOT_MATCH(HttpStatus.BAD_REQUEST, "MEMBER4005", "현재 비밀번호가 일치하지 않습니다.") + PASSWORD_NOT_MATCH(HttpStatus.BAD_REQUEST, "MEMBER4005", "현재 비밀번호가 일치하지 않습니다."), + SAME_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4006", "새 비밀번호가 현재 비밀번호와 동일합니다.") ; private final HttpStatus httpStatus; From c12ac3670dd29faa9e60fed8e59845b8e2b4f588 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Wed, 11 Feb 2026 22:09:45 +0900 Subject: [PATCH 79/83] =?UTF-8?q?[Chore]=20=EB=93=A4=EC=97=AC=EC=93=B0?= =?UTF-8?q?=EA=B8=B0=20=EC=9D=BC=EC=B9=98=EC=8B=9C=ED=82=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatsfine/global/config/SecurityConfig.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java index e4400dd0..c9b457bd 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java @@ -1,20 +1,20 @@ package com.eatsfine.eatsfine.global.config; +import com.eatsfine.eatsfine.domain.user.exception.handler.CustomOAuth2FailureHandler; +import com.eatsfine.eatsfine.domain.user.exception.handler.CustomOAuth2SuccessHandler; +import com.eatsfine.eatsfine.domain.user.service.oauthService.CustomOAuth2MemberServiceImpl; import com.eatsfine.eatsfine.global.auth.CustomAccessDeniedHandler; import com.eatsfine.eatsfine.global.auth.CustomAuthenticationEntryPoint; import com.eatsfine.eatsfine.global.auth.HttpCookieOAuth2AuthorizationRequestRepository; import com.eatsfine.eatsfine.global.config.jwt.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpMethod; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @@ -90,9 +90,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .successHandler(customOAuth2SuccessHandler) .failureHandler(customOAuth2FailureHandler)) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - return http.build(); + return http.build(); } @Bean From dc1d4eef4b180ebf128c791ac33c010eeebb6f6a Mon Sep 17 00:00:00 2001 From: SungMinju Date: Wed, 11 Feb 2026 22:25:42 +0900 Subject: [PATCH 80/83] =?UTF-8?q?[Fix]=20Origin=20=EA=B0=92=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/SecurityConfig.java | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java index c9b457bd..ecd6de33 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java @@ -100,20 +100,24 @@ public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequest return new HttpCookieOAuth2AuthorizationRequestRepository(); } - @Bean - public CorsConfigurationSource corsConfigurationSource() { // cors 설정 - CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOriginPatterns(List.of("*")); // 운영 환경에서는 정확한 도메인만 명시 - config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); - config.setAllowedHeaders(List.of("*")); - config.setExposedHeaders(List.of("Authorization", "Set-Cookie")); // 쿠키, Authorization 헤더 노출 - config.setAllowCredentials(true); - config.setMaxAge(Duration.ofHours(1)); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); - return source; - } + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + + config.setAllowedOrigins(List.of( + "http://localhost:5173", + "https://eatsfine.co.kr" + )); + config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS","PATCH")); + config.setAllowedHeaders(List.of("Content-Type", "Authorization", "X-Requested-With")); + config.setExposedHeaders(List.of("Authorization", "Set-Cookie")); + config.setAllowCredentials(true); + config.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } @Bean public PasswordEncoder passwordEncoder() { From 1723fa224ee4b256831a6ab758b5ba738ebd923c Mon Sep 17 00:00:00 2001 From: SungMinju Date: Thu, 12 Feb 2026 02:56:33 +0900 Subject: [PATCH 81/83] =?UTF-8?q?[Debug]=20=ED=9A=8C=EC=9B=90=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=20=EB=B0=8F=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/converter/UserConverter.java | 4 ++-- .../eatsfine/domain/user/entity/User.java | 20 +++++++++++++++++++ .../service/userService/UserServiceImpl.java | 16 ++++++++++----- .../domain/user/status/UserErrorStatus.java | 3 ++- .../global/config/SecurityConfig.java | 4 ++-- 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java index d99117ff..0f1d6ebe 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java @@ -30,10 +30,10 @@ public static UserResponseDto.LoginResponseDto toLoginResponse(User user, String // 유저 정보 조회 응답 변환 - public static UserResponseDto.UserInfoDto toUserInfo(User user) { + public static UserResponseDto.UserInfoDto toUserInfo(User user, String profileImageUrl) { return UserResponseDto.UserInfoDto.builder() .id(user.getId()) - .profileImage(user.getProfileImage()) + .profileImage(profileImageUrl) .email(user.getEmail()) .name(user.getName()) .phoneNumber(user.getPhoneNumber()) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java index bd2fc6e3..e232c147 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java @@ -6,6 +6,9 @@ import com.eatsfine.eatsfine.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; + +import java.time.LocalDateTime; + @Entity @Getter // 수정한 부분: access 레벨을 PROTECTED로 설정하여 Hibernate가 접근할 수 있게 합니다. @@ -46,6 +49,12 @@ public class User extends BaseEntity { @Column(length = 500) private String refreshToken; + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + @Column(name = "is_deleted") + private Boolean isDeleted = false; + public void updateName(String name) { this.name = name; } @@ -87,6 +96,17 @@ public void linkSocial (SocialType socialType, String socialId){ this.socialId = socialId; } + // 회원 탈퇴 메서드 추가 + public void withdraw() { + this.isDeleted = true; + this.deletedAt = LocalDateTime.now(); + this.refreshToken = null; // refresh token도 null 처리 + } + + public boolean isDeleted() { + return this.isDeleted != null && this.isDeleted; + } + @OneToOne(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) private Term term; } diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java index f773a80f..325400e4 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java @@ -85,12 +85,12 @@ public UserResponseDto.LoginResponseDto login(UserRequestDto.LoginDto loginDto) .refreshToken(refreshToken) .build(); } - @Override @Transactional public UserResponseDto.UserInfoDto getMemberInfo(HttpServletRequest request) { User user = getCurrentUser(request); - return UserConverter.toUserInfo(user); + String profileUrl = s3Service.toUrl(user.getProfileImage()); + return UserConverter.toUserInfo(user, profileUrl); } @Override @@ -191,6 +191,7 @@ private void validateProfileImage(MultipartFile file) { } + @Override @Transactional public void withdraw(HttpServletRequest request) { @@ -205,8 +206,8 @@ public void withdraw(HttpServletRequest request) { } } - user.updateRefreshToken(null); - userRepository.delete(user); + user.withdraw(); + userRepository.save(user); } @Override @@ -225,8 +226,13 @@ private User getCurrentUser(HttpServletRequest request) { String email = jwtTokenProvider.getEmailFromToken(token); - return userRepository.findByEmail(email) + User user = userRepository.findByEmail(email) .orElseThrow(() -> new UserException(UserErrorStatus.MEMBER_NOT_FOUND)); + + if (user.isDeleted()) { + throw new UserException(UserErrorStatus.WITHDRAWN_USER); + } + return user; } @Override diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java index 5041af6e..b3f94c78 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java @@ -16,7 +16,8 @@ public enum UserErrorStatus implements BaseErrorCode { EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER4003", "이미 존재하는 이메일입니다."), INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4004", "비밀번호가 올바르지 않습니다."), PASSWORD_NOT_MATCH(HttpStatus.BAD_REQUEST, "MEMBER4005", "현재 비밀번호가 일치하지 않습니다."), - SAME_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4006", "새 비밀번호가 현재 비밀번호와 동일합니다.") + SAME_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4006", "새 비밀번호가 현재 비밀번호와 동일합니다."), + WITHDRAWN_USER(HttpStatus.FORBIDDEN, "MEMBER4007", "탈퇴한 회원입니다."); ; diff --git a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java index ecd6de33..55cdd4e4 100644 --- a/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java +++ b/src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java @@ -101,7 +101,7 @@ public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequest } @Bean - public CorsConfigurationSource corsConfigurationSource() { + public CorsConfigurationSource corsConfigurationSource() { // cors 설정 CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of( @@ -110,7 +110,7 @@ public CorsConfigurationSource corsConfigurationSource() { )); config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS","PATCH")); config.setAllowedHeaders(List.of("Content-Type", "Authorization", "X-Requested-With")); - config.setExposedHeaders(List.of("Authorization", "Set-Cookie")); + config.setExposedHeaders(List.of("Authorization", "Set-Cookie")); // 쿠키, Authorization 헤더 노출 config.setAllowCredentials(true); config.setMaxAge(3600L); From 50417c0d441d149330f934ed56bd11a670baa9e9 Mon Sep 17 00:00:00 2001 From: J_lisa&& <155425927+SungMinju@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:35:42 +0900 Subject: [PATCH 82/83] Update UserErrorStatus.java --- .../eatsfine/eatsfine/domain/user/status/UserErrorStatus.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java index 34019ec2..a4c8b147 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java @@ -17,7 +17,7 @@ public enum UserErrorStatus implements BaseErrorCode { INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4004", "비밀번호가 올바르지 않습니다."), PASSWORD_NOT_MATCH(HttpStatus.BAD_REQUEST, "MEMBER4005", "현재 비밀번호가 일치하지 않습니다."), SAME_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4006", "새 비밀번호가 현재 비밀번호와 동일합니다."), - WITHDRAWN_USER(HttpStatus.FORBIDDEN, "MEMBER4007", "탈퇴한 회원입니다."); + WITHDRAWN_USER(HttpStatus.FORBIDDEN, "MEMBER4007", "탈퇴한 회원입니다.") ; private final HttpStatus httpStatus; From 32f66971b7bce6196b066a5c159130e24cfb176c Mon Sep 17 00:00:00 2001 From: J_lisa&& <155425927+SungMinju@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:36:45 +0900 Subject: [PATCH 83/83] Update UserServiceImpl.java --- .../domain/user/service/userService/UserServiceImpl.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java index 325400e4..fdd8dc38 100644 --- a/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java +++ b/src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java @@ -67,6 +67,10 @@ public UserResponseDto.LoginResponseDto login(UserRequestDto.LoginDto loginDto) User user = userRepository.findByEmail(loginDto.getEmail()) .orElseThrow(() -> new UserException(UserErrorStatus.MEMBER_NOT_FOUND)); + if (user.isDeleted()) { + throw new UserException(UserErrorStatus.WITHDRAWN_USER); + } + // 2) 비밀번호 검증 if (!passwordEncoder.matches(loginDto.getPassword(), user.getPassword())) { throw new UserException(UserErrorStatus.INVALID_PASSWORD); @@ -289,4 +293,4 @@ public UserResponseDto.UpdatePasswordDto changePassword( return UserConverter.toUpdatePasswordResponse(true, LocalDateTime.now(), "비밀번호가 성공적으로 변경되었습니다."); } -} \ No newline at end of file +}