From 442fb475396dc56aa843f746a5bdbb4c4a28ae39 Mon Sep 17 00:00:00 2001 From: SungMinju Date: Mon, 12 Jan 2026 20:37:40 +0900 Subject: [PATCH 01/41] =?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 5ec34cb..45f3d74 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 c0e73d2..347e5c2 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 8bf83dc..2fbb4fd 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 3b4956c..9781fca 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 1bb000e..f4a9e77 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 a9d9405..5ecf6ad 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/41] =?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 6db43f2..c2a851e 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/41] =?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 4e82f3f..3a2ac99 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 0c53844..b8b749b 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 9528c2e..0000000 --- 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 0000000..d3e2476 --- /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 0000000..eab7775 --- /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 d6279e5..dd319bb 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 0000000..6f0b61c --- /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 0000000..857808c --- /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 0000000..a0ef11b --- /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 6caff48..734fa4c 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 d4457ff..8f2bbdd 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 0000000..14190ba --- /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 0000000..18d6857 --- /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 5654812..cdcfc10 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/41] =?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 0000000..114cce1 --- /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 0000000..e7148b9 --- /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 0000000..9f6ce7b --- /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 0000000..67028e0 --- /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 0000000..d3e0d94 --- /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 0000000..c81b91a --- /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/41] =?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 761d33b..5ca5761 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/41] =?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 d3e2476..703b1f0 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 dd319bb..375f31f 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 6caff48..b1614ca 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/41] =?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 ddce98d..4412057 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/41] =?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 e7148b9..c27eba7 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/41] =?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 5654812..72e2e1a 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/41] =?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 703b1f0..91d903c 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 eab7775..0b7f8d7 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 5ca5761..40ea069 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/41] =?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 4412057..a9f0822 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 8f2bbdd..96fe2e5 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 0ce5331..175073b 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/41] =?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 67028e0..f578c36 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/41] =?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 0000000..3bc78a0 --- /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/41] =?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 91d903c..0814962 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/41] =?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 0814962..64dc61f 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/41] =?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 0b7f8d7..24e801f 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/41] =?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 a9f0822..9227442 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/41] =?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 9227442..824fae7 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 24e801f..a81b033 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/41] =?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 0000000..8c4ea21 --- /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 0000000..b9ee691 --- /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 0000000..190a188 --- /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 0000000..8b1a15f --- /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 9f6ce7b..dffeb0d 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/41] =?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 0000000..6279705 --- /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/41] =?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 8e2f8f8..30dc44e 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 c0e73d2..5fcf469 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 f38ab2f..0c50b23 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/41] =?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 375f31f..818a0f0 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 b1614ca..12bca8a 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/41] =?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 c27eba7..704609c 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/41] =?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 b8b749b..955b0e6 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 6d3ef6b..1818c90 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 96fe2e5..7fb4448 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/41] =?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 3717efd..61d4b72 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/41] =?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 d617055..ed73e90 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 83119ee..3c38e43 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 ec8978d..2eaca4c 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/41] =?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 ef35d7c..fd69063 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/41] =?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 8b1a15f..ced45b4 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/41] =?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 cfd9740..a024e44 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 704609c..c6a1907 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/41] =?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 7fb4448..72ab36b 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/41] =?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 64dc61f..023883e 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/41] =?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 955b0e6..1f6ee1b 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 72ab36b..de5b703 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/41] =?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 3bc78a0..4934fed 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/41] =?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 4934fed..cf0f497 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 0000000..1c1c8d7 --- /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/41] =?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 6279705..2807b4a 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 14190ba..d9f16cc 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/41] =?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 175073b..6fd130c 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/41] =?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 824fae7..81c3c0d 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/41] =?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 de5b703..4970646 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/41] =?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 1f6ee1b..91e28b3 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 ced45b4..f55d5cc 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 c81b91a..dd85c4f 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/41] =?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 190a188..c8da939 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/41] =?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 023883e..e368ada 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 = "현재 비밀번호는 필수입니다.")