Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,7 @@ public class MemberController {
public ApiResponse<MemberResDTO.SignUp> 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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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())
Comment on lines +21 to +27
// 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()
Expand Down
13 changes: 10 additions & 3 deletions src/main/java/com/example/umc10th/domain/member/entity/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.umc10th.domain.member.enums;

public enum SocialType {
LOCAL, // 폼 회원가입 (이메일/비밀번호)
KAKAO, NAVER, APPLE, GOOGLE
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Food, Long> {
Optional<Food> findByName(com.example.umc10th.domain.member.enums.Food name);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 메서드를 자동으로 구현해준다.
// <Member, Long>: 이 Repository는 Member 엔터티를 관리하며, Member 엔터티의 기본 키(PK) 타입이 Long임을 나타낸다.
public interface MemberRepository extends JpaRepository<Member, Long> {

// 회원가입 시 이메일 중복 체크용
boolean existsByEmail(String email);

// 로그인에서 사용자 이메일로 회원 조회
Optional<Member> findByEmail(String email);
}
Original file line number Diff line number Diff line change
@@ -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<Term, Long> {
Optional<Term> findByName(com.example.umc10th.domain.member.enums.Term name);
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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);
}
Comment on lines +60 to +78

// 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Comment on lines +15 to +20

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);
});
Comment on lines +32 to +41
}

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);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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;
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
@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/**",

// 폼 로그인 페이지 자체
"/login",
"/login/**"
};
Comment on lines +23 to +33

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(requests -> requests
.requestMatchers(allowUris).permitAll()
Comment on lines +36 to +40
.anyRequest().authenticated()
)
.exceptionHandling(exception -> exception
.authenticationEntryPoint(customEntryPoint)
.accessDeniedHandler(customAccessDenied)
)
.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();
}
}
Loading