From 3f861c730a814ef140c6509ac1577687bc8bcd26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=92=E1=85=A1=E1=84=85?= =?UTF-8?q?=E1=85=B5=E1=86=B7?= Date: Fri, 22 May 2026 16:56:27 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20SocialType=20enum=EC=97=90=20LOCA?= =?UTF-8?q?L=20=EB=B0=8F=20=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=EC=9A=A9=20enum=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/umc10th/domain/member/enums/SocialType.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java b/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java index 4367db5..05ccf6b 100644 --- a/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java +++ b/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java @@ -1,5 +1,4 @@ package com.example.umc10th.domain.member.enums; public enum SocialType { - KAKAO, NAVER, APPLE, GOOGLE } From bc37e63a946bc2bf13b6670cc243bee2ae895e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=92=E1=85=A1=E1=84=85?= =?UTF-8?q?=E1=85=B5=E1=86=B7?= Date: Fri, 22 May 2026 17:14:53 +0900 Subject: [PATCH 02/14] =?UTF-8?q?chore:=20Spring=20Security=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index 03cd742..1b4d44b 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' From 63075c83b149036c6b500a7101728037d672c1f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=92=E1=85=A1=E1=84=85?= =?UTF-8?q?=E1=85=B5=E1=86=B7?= Date: Fri, 22 May 2026 17:17:50 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20SocialType=20enum=EC=97=90=20LOCA?= =?UTF-8?q?L(=ED=8F=BC=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=EC=9A=A9)=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/example/umc10th/domain/member/enums/SocialType.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java b/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java index 05ccf6b..43e9e75 100644 --- a/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java +++ b/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java @@ -1,4 +1,6 @@ package com.example.umc10th.domain.member.enums; public enum SocialType { + LOCAL, // 폼 회원가입 (이메일/비밀번호) + KAKAO, NAVER, APPLE, GOOGLE } From 1ba096c7ab326e7df30311a27d8c0a8dddcb7c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=92=E1=85=A1=E1=84=85?= =?UTF-8?q?=E1=85=B5=E1=86=B7?= Date: Fri, 22 May 2026 17:28:52 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20password=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=86=8C=EC=85=9C=20=EC=B9=BC?= =?UTF-8?q?=EB=9F=BC=20nullable=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/member/entity/Member.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/src/main/java/com/example/umc10th/domain/member/entity/Member.java index 87f3bfd..32075dd 100644 --- a/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -48,12 +48,17 @@ public class Member extends BaseEntity { @Column(name = "detail_address", nullable = false, length = 255) private String detailAddress; - @Column(name = "social_uid", nullable = false, length = 255) + @Column(name = "social_uid", length = 255) private String socialUid; @Enumerated(EnumType.STRING) @Column(name = "social_type", nullable = false) - private SocialType socialType; + @Builder.Default + private SocialType socialType = SocialType.LOCAL; + + // BCrypt 해시 결과는 60자 고정이지만, 알고리즘 변경 대비 여유롭게 100자로 설정 + @Column(nullable = false, length = 100) + private String password; @Column(nullable = false) @Builder.Default @@ -66,8 +71,10 @@ public class Member extends BaseEntity { private String phoneNumber; // columnDefinition = "TEXT": 255자 이상의 대용량 텍스트 저장 가능 타입으로 지정 + // 기본 프로필 URL - 회원가입 시 따로 안 받으므로 기본값 박기 @Column(name = "profile_url", columnDefinition = "TEXT", nullable = false) - private String profileUrl; + @Builder.Default + private String profileUrl = "https://default-profile.example.com/default.png"; // 연관 관계 @OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE) From dc2f1509fe5a378e849bd5b9d7b06fc35be35fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=92=E1=85=A1=E1=84=85?= =?UTF-8?q?=E1=85=B5=E1=86=B7?= Date: Fri, 22 May 2026 23:19:49 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20MemberRepository=EC=97=90=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=A1=B0=ED=9A=8C=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=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/member/repository/MemberRepository.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index a73b7c5..157519a 100644 --- a/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -3,7 +3,15 @@ import com.example.umc10th.domain.member.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + // JpaRepository: Spring Data JPA가 제공하는 인터페이스로, 기본적인 CRUD 메서드를 자동으로 구현해준다. // : 이 Repository는 Member 엔터티를 관리하며, Member 엔터티의 기본 키(PK) 타입이 Long임을 나타낸다. public interface MemberRepository extends JpaRepository { + + // 회원가입 시 이메일 중복 체크용 + boolean existsByEmail(String email); + + // 로그인에서 사용자 이메일로 회원 조회 + Optional findByEmail(String email); } From d8f70900b510efa88e0a46114fa88e24416a75ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=92=E1=85=A1=E1=84=85?= =?UTF-8?q?=E1=85=B5=E1=86=B7?= Date: Sat, 23 May 2026 00:09:29 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20Food,=20Term=20=EB=A0=88=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20?= =?UTF-8?q?CommaneLineRunner=EB=A1=9C=20=EC=B4=88=EA=B8=B0=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=84=A3=EC=96=B4=EC=A3=BC=EA=B8=B0=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 --- .../member/repository/FoodRepository.java | 10 ++++ .../member/repository/TermRepository.java | 10 ++++ .../umc10th/global/config/InitDataLoader.java | 54 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 src/main/java/com/example/umc10th/domain/member/repository/FoodRepository.java create mode 100644 src/main/java/com/example/umc10th/domain/member/repository/TermRepository.java create mode 100644 src/main/java/com/example/umc10th/global/config/InitDataLoader.java diff --git a/src/main/java/com/example/umc10th/domain/member/repository/FoodRepository.java b/src/main/java/com/example/umc10th/domain/member/repository/FoodRepository.java new file mode 100644 index 0000000..56e69e4 --- /dev/null +++ b/src/main/java/com/example/umc10th/domain/member/repository/FoodRepository.java @@ -0,0 +1,10 @@ +package com.example.umc10th.domain.member.repository; + +import com.example.umc10th.domain.member.entity.Food; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface FoodRepository extends JpaRepository { + Optional findByName(com.example.umc10th.domain.member.enums.Food name); +} diff --git a/src/main/java/com/example/umc10th/domain/member/repository/TermRepository.java b/src/main/java/com/example/umc10th/domain/member/repository/TermRepository.java new file mode 100644 index 0000000..4157d32 --- /dev/null +++ b/src/main/java/com/example/umc10th/domain/member/repository/TermRepository.java @@ -0,0 +1,10 @@ +package com.example.umc10th.domain.member.repository; + +import com.example.umc10th.domain.member.entity.Term; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface TermRepository extends JpaRepository { + Optional findByName(com.example.umc10th.domain.member.enums.Term name); +} diff --git a/src/main/java/com/example/umc10th/global/config/InitDataLoader.java b/src/main/java/com/example/umc10th/global/config/InitDataLoader.java new file mode 100644 index 0000000..a757e4a --- /dev/null +++ b/src/main/java/com/example/umc10th/global/config/InitDataLoader.java @@ -0,0 +1,54 @@ +package com.example.umc10th.global.config; + +import com.example.umc10th.domain.member.entity.Food; +import com.example.umc10th.domain.member.entity.Term; +import com.example.umc10th.domain.member.repository.FoodRepository; +import com.example.umc10th.domain.member.repository.TermRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; + +@Slf4j +@Component +@RequiredArgsConstructor +// CommandLineRunner: Spring이 제공하는 인터페이스 +// 이걸 구현하면 애플리케이션이 시작된 직후에 run() 메서드가 자동으로 호출된다. +public class InitDataLoader implements CommandLineRunner { + + private final FoodRepository foodRepository; + private final TermRepository termRepository; + + @Override + @Transactional + public void run(String... args) { + seedFoods(); + seedTerms(); + } + + private void seedFoods() { + // enum의 모든 값을 순회하면서, DB에 없는 것만 INSERT + Arrays.stream(com.example.umc10th.domain.member.enums.Food.values()) + .filter(foodEnum -> foodRepository.findByName(foodEnum).isEmpty()) + .forEach(foodEnum -> { + foodRepository.save(Food.builder() + .name(foodEnum) + .build()); + log.info("[InitDataLoader] Food 시드: {}", foodEnum); + }); + } + + private void seedTerms() { + Arrays.stream(com.example.umc10th.domain.member.enums.Term.values()) + .filter(termEnum -> termRepository.findByName(termEnum).isEmpty()) + .forEach(termEnum -> { + termRepository.save(Term.builder() + .name(termEnum) + .build()); + log.info("[InitDataLoader] Term 시드: {}", termEnum); + }); + } +} From 4d87855097833475844e2549a5a8966f5a67a243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=92=E1=85=A1=E1=84=85?= =?UTF-8?q?=E1=85=B5=E1=86=B7?= Date: Sat, 23 May 2026 00:23:00 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20MemberErrorCode=EC=97=90=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=BD=94=EB=93=9C=20enum=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/exception/code/MemberErrorCode.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index 9d62f9c..367b3e1 100644 --- a/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -9,7 +9,11 @@ @RequiredArgsConstructor public enum MemberErrorCode implements BaseErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "회원을 찾을 수 없습니다."), - MEMBER_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER409_1", "이미 존재하는 유저입니다."); + MEMBER_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER409_1", "이미 존재하는 유저입니다."), + + // 회원가입 관련 에러 + FOOD_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER400_1", "유효하지 않은 선호 음식입니다."), + TERM_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER400_2", "유효하지 않은 약관입니다."); private final HttpStatus status; private final String code; From fba041c22e6c836cec0dd7092dc3221debc1376e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=92=E1=85=A1=E1=84=85?= =?UTF-8?q?=E1=85=B5=E1=86=B7?= Date: Sat, 23 May 2026 00:32:37 +0900 Subject: [PATCH 08/14] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B4=80=EB=A0=A8=20=EB=B3=80=ED=99=98=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=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 --- .../member/converter/MemberConverter.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index 4a69730..e01cfa2 100644 --- a/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -1,15 +1,44 @@ package com.example.umc10th.domain.member.converter; +import com.example.umc10th.domain.member.dto.MemberReqDTO; import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.enums.Gender; import com.example.umc10th.domain.mission.entity.mapping.MemberMission; import com.example.umc10th.global.dto.PageInfoDTO; +import com.example.umc10th.global.enums.Address; import org.springframework.data.domain.Page; import java.util.List; public class MemberConverter { + // 회원가입: 요청 DTO -> Member 엔티티 + public static Member toMember(MemberReqDTO.SignUp request, String encodedPassword) { + return Member.builder() + .name(request.name()) + .nickname(request.nickname()) + .email(request.email()) + .password(encodedPassword) + .phoneNumber(request.phoneNumber()) + .gender(Gender.valueOf(request.gender())) + .birth(request.birth()) + .address(Address.valueOf(request.address())) + .detailAddress(request.detailAddress()) + // socialType, profileUrl, point는 엔티티의 @Builder.Default로 자동 처리 + // (LOCAL, 기본 프로필 URL, 0) + .build(); + } + + // 회원가입: 저장된 Member 엔티티 -> 회원가입 응답 DTO + public static MemberResDTO.SignUp toSignupResponse(Member member) { + return MemberResDTO.SignUp.builder() + .memberId(member.getId()) + .nickname(member.getNickname()) + .email(member.getEmail()) + .build(); + } + // 마이페이지 변환 public static MemberResDTO.MyPage toMyPage(Member member) { return MemberResDTO.MyPage.builder() From 953618be85c633198004747f907fac67d4fc4a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=92=E1=85=A1=E1=84=85?= =?UTF-8?q?=E1=85=B5=E1=86=B7?= Date: Sat, 23 May 2026 00:56:25 +0900 Subject: [PATCH 09/14] =?UTF-8?q?feat:=20SecurityConfig=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=B4=20=EC=8A=A4=ED=94=84=EB=A7=81=20=EC=8B=9C?= =?UTF-8?q?=ED=81=90=EB=A6=AC=ED=8B=B0=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/config/SecurityConfig.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/main/java/com/example/umc10th/global/config/SecurityConfig.java diff --git a/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/src/main/java/com/example/umc10th/global/config/SecurityConfig.java new file mode 100644 index 0000000..23df7c5 --- /dev/null +++ b/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -0,0 +1,49 @@ +package com.example.umc10th.global.config; + +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.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@EnableWebSecurity +@Configuration +public class SecurityConfig { + + private final String[] allowUris = { + // Swagger 허용 + "/swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + "/auth/**" + }; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(requests -> requests + .requestMatchers(allowUris).permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form + .defaultSuccessUrl("/swagger-ui/index.html", true) + .permitAll() + ) + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .permitAll() + ); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file From b892cb8999d5772038597ef54d21081201f861b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=92=E1=85=A1=E1=84=85?= =?UTF-8?q?=E1=85=B5=E1=86=B7?= Date: Sat, 23 May 2026 01:31:30 +0900 Subject: [PATCH 10/14] =?UTF-8?q?feat:=20MemberService=EC=97=90=20signUp?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=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/member/service/MemberService.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index 7cb5069..7a5ad3f 100644 --- a/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -1,17 +1,27 @@ package com.example.umc10th.domain.member.service; import com.example.umc10th.domain.member.converter.MemberConverter; +import com.example.umc10th.domain.member.dto.MemberReqDTO; import com.example.umc10th.domain.member.dto.MemberResDTO; +import com.example.umc10th.domain.member.entity.Food; import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.entity.Term; +import com.example.umc10th.domain.member.entity.mapping.MemberFood; +import com.example.umc10th.domain.member.entity.mapping.MemberTerm; import com.example.umc10th.domain.member.exception.MemberException; import com.example.umc10th.domain.member.exception.code.MemberErrorCode; +import com.example.umc10th.domain.member.repository.FoodRepository; import com.example.umc10th.domain.member.repository.MemberRepository; +import com.example.umc10th.domain.member.repository.TermRepository; import com.example.umc10th.domain.mission.entity.mapping.MemberMission; import com.example.umc10th.domain.mission.repository.MemberMissionRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -24,6 +34,71 @@ public class MemberService { private final MemberRepository memberRepository; private final MemberMissionRepository memberMissionRepository; + private final FoodRepository foodRepository; + private final TermRepository termRepository; + private final PasswordEncoder passwordEncoder; + + @PersistenceContext + private EntityManager em; // MemberFood/MemberTerm을 직접 persist하기 위해 + + // 회원가입 + @Transactional + public MemberResDTO.SignUp signUp(MemberReqDTO.SignUp request) { + + // 1. 이메일 중복 체크 + if (memberRepository.existsByEmail(request.email())) { + throw new MemberException(MemberErrorCode.MEMBER_ALREADY_EXISTS); + } + + // 2. 비밀번호 BCrypt 인코딩 + String encodedPassword = passwordEncoder.encode(request.password()); + + // 3. Member 엔티티 생성 및 저장 + Member member = MemberConverter.toMember(request, encodedPassword); + Member savedMember = memberRepository.save(member); + + // 4. 선호 음식 매핑 저장 + for (String foodName: request.preferredFoods()) { + com.example.umc10th.domain.member.enums.Food foodEnum; + try { + foodEnum = com.example.umc10th.domain.member.enums.Food.valueOf(foodName); + } catch (IllegalArgumentException e) { + throw new MemberException(MemberErrorCode.FOOD_NOT_FOUND); + } + + Food food = foodRepository.findByName(foodEnum) + .orElseThrow(() -> new MemberException(MemberErrorCode.FOOD_NOT_FOUND)); + + MemberFood memberFood = MemberFood.builder() + .member(savedMember) + .food(food) + .build(); + + em.persist(memberFood); + } + + // 5. 약관 동의 매핑 저장 + for (String termName: request.agreedTerms()) { + com.example.umc10th.domain.member.enums.Term termEnum; + try { + termEnum = com.example.umc10th.domain.member.enums.Term.valueOf(termName); + } catch (IllegalArgumentException e) { + throw new MemberException(MemberErrorCode.TERM_NOT_FOUND); + } + + Term term = termRepository.findByName(termEnum) + .orElseThrow(() -> new MemberException(MemberErrorCode.TERM_NOT_FOUND)); + + MemberTerm memberTerm = MemberTerm.builder() + .member(savedMember) + .term(term) + .build(); + + em.persist(memberTerm); + } + + return MemberConverter.toSignupResponse(savedMember); + } // 마이페이지 public MemberResDTO.MyPage getMyPage(Long memberId) { From c8111dc0e5b71b47349eb336ec36a60f143bc68e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=92=E1=85=A1=E1=84=85?= =?UTF-8?q?=E1=85=B5=E1=86=B7?= Date: Sat, 23 May 2026 01:34:00 +0900 Subject: [PATCH 11/14] =?UTF-8?q?feat:=20MemberController=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20mock=20=EC=9D=91=EB=8B=B5=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=ED=95=98=EA=B3=A0=20service=20=ED=98=B8=EC=B6=9C=EB=A1=9C=20?= =?UTF-8?q?=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/controller/MemberController.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index 731557e..d1ad441 100644 --- a/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -25,13 +25,7 @@ public class MemberController { public ApiResponse signUp( @Valid @RequestBody MemberReqDTO.SignUp request ) { - // 6주차에서 memberService.signUp(request)로 교체 - MemberResDTO.SignUp result = MemberResDTO.SignUp.builder() - .memberId(1L) - .nickname(request.nickname()) - .email(request.email()) - .build(); - + MemberResDTO.SignUp result = memberService.signUp(request); return ApiResponse.onSuccess(MemberSuccessCode.SIGN_UP, result); } From b0779f9eedd6e53f78f6da82e3b9ba7872c3eb53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=92=E1=85=A1=E1=84=85?= =?UTF-8?q?=E1=85=B5=E1=86=B7?= Date: Sat, 23 May 2026 04:01:13 +0900 Subject: [PATCH 12/14] =?UTF-8?q?feat:=20AuthMember(UserDetails=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=B2=B4)=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/security/AuthMember.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/main/java/com/example/umc10th/global/security/AuthMember.java diff --git a/src/main/java/com/example/umc10th/global/security/AuthMember.java b/src/main/java/com/example/umc10th/global/security/AuthMember.java new file mode 100644 index 0000000..91328dc --- /dev/null +++ b/src/main/java/com/example/umc10th/global/security/AuthMember.java @@ -0,0 +1,35 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.domain.member.entity.Member; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class AuthMember implements UserDetails { + + private final Member member; + + @Override + public Collection getAuthorities() { + // 지금은 단일 권한 ROLE_USER만 + // 나중에 ADMIN 등 추가 시 여기서 분기 + return List.of(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Override + public String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getEmail(); + } +} From 7abdc4eb507badcb8bfbdddabc6882d0d9ef44fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=92=E1=85=A1=E1=84=85?= =?UTF-8?q?=E1=85=B5=E1=86=B7?= Date: Sat, 23 May 2026 04:17:46 +0900 Subject: [PATCH 13/14] =?UTF-8?q?feat:=20CustomUserDetailsService=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/CustomUserDetailsService.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java diff --git a/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java b/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java new file mode 100644 index 0000000..6a77ff6 --- /dev/null +++ b/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java @@ -0,0 +1,25 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +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; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException( + "해당 이메일의 회원을 찾을 수 없습니다: " + email + )); + return new AuthMember(member); + } +} From a7c7c23f3cdaaeee530c29ae79c83b2c91c7d8f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=92=E1=85=A1=E1=84=85?= =?UTF-8?q?=E1=85=B5=E1=86=B7?= Date: Sat, 23 May 2026 04:54:13 +0900 Subject: [PATCH 14/14] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D,=20=EC=9D=B8?= =?UTF-8?q?=EA=B0=80=20=EC=8B=A4=ED=8C=A8=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/config/SecurityConfig.java | 17 +++++++++++- .../global/security/CustomAccessDenied.java | 27 +++++++++++++++++++ .../global/security/CustomEntryPoint.java | 27 +++++++++++++++++++ .../security/SecurityResponseWriter.java | 26 ++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java create mode 100644 src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java create mode 100644 src/main/java/com/example/umc10th/global/security/SecurityResponseWriter.java diff --git a/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 23df7c5..130840a 100644 --- a/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -1,5 +1,8 @@ package com.example.umc10th.global.config; +import com.example.umc10th.global.security.CustomAccessDenied; +import com.example.umc10th.global.security.CustomEntryPoint; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -11,14 +14,22 @@ @EnableWebSecurity @Configuration +@RequiredArgsConstructor public class SecurityConfig { + private final CustomEntryPoint customEntryPoint; + private final CustomAccessDenied customAccessDenied; + private final String[] allowUris = { // Swagger 허용 "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**", - "/auth/**" + "/auth/**", + + // 폼 로그인 페이지 자체 + "/login", + "/login/**" }; @Bean @@ -29,6 +40,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers(allowUris).permitAll() .anyRequest().authenticated() ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(customEntryPoint) + .accessDeniedHandler(customAccessDenied) + ) .formLogin(form -> form .defaultSuccessUrl("/swagger-ui/index.html", true) .permitAll() diff --git a/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java b/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java new file mode 100644 index 0000000..09d9640 --- /dev/null +++ b/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java @@ -0,0 +1,27 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class CustomAccessDenied implements AccessDeniedHandler { + + private final SecurityResponseWriter responseWriter; + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException { + responseWriter.write(response, GeneralErrorCode.FORBIDDEN); + } +} diff --git a/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java b/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java new file mode 100644 index 0000000..8b6d29b --- /dev/null +++ b/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java @@ -0,0 +1,27 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class CustomEntryPoint implements AuthenticationEntryPoint { + + private final SecurityResponseWriter responseWriter; + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException { + responseWriter.write(response, GeneralErrorCode.UNAUTHORIZED); + } +} diff --git a/src/main/java/com/example/umc10th/global/security/SecurityResponseWriter.java b/src/main/java/com/example/umc10th/global/security/SecurityResponseWriter.java new file mode 100644 index 0000000..e09fca5 --- /dev/null +++ b/src/main/java/com/example/umc10th/global/security/SecurityResponseWriter.java @@ -0,0 +1,26 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class SecurityResponseWriter { + + private ObjectMapper objectMapper; + + public void write(HttpServletResponse response, BaseErrorCode code) throws IOException { + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + ApiResponse errorResponse = ApiResponse.onFailure(code, null); + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +}