Skip to content
Merged
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
34 changes: 12 additions & 22 deletions src/main/java/com/arom/with_travel/domain/member/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.arom.with_travel.domain.community_reply.CommunityReplyLike;
import com.arom.with_travel.domain.image.Image;
import com.arom.with_travel.domain.likes.Likes;
import com.arom.with_travel.domain.member.dto.request.MemberSignupRequestDto;
import com.arom.with_travel.domain.shorts.Shorts;
import com.arom.with_travel.domain.shorts_reply.ShortsReply;
import com.arom.with_travel.domain.survey.Survey;
Expand All @@ -29,6 +30,8 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SQLDelete(sql = "UPDATE member SET is_deleted = true, deleted_at = now() where id = ?")
@SQLRestriction("is_deleted is FALSE")
@Builder
@AllArgsConstructor
public class Member extends BaseEntity {

@Id
Expand Down Expand Up @@ -70,7 +73,8 @@ public enum Gender {
}

public enum LoginType {
KAKAO
KAKAO,
LOCAL
}

public Member(String memberName, String email, Role role) {
Expand Down Expand Up @@ -129,25 +133,6 @@ public static Member create(String memberName, String email, String oauthId) {
@OneToOne(mappedBy = "member")
private Image image;

@Builder
public Member(Long id, String oauthId, String email, String password, String name,
LocalDate birth, Gender gender, String phone, LoginType loginType,
String nickname, String introduction, TravelType travelType, Role role) {
this.id = id;
this.oauthId = oauthId;
this.email = email;
this.password = password;
this.name = name;
this.birth = birth;
this.gender = gender;
this.phone = phone;
this.loginType = loginType;
this.nickname = nickname;
this.introduction = introduction;
this.travelType = travelType;
this.role = role;
}

public void validateNotAlreadyAppliedTo(Accompany accompany) {
boolean alreadyApplied = accompanyApplies.stream()
.anyMatch(apply -> apply.getAccompany().equals(accompany));
Expand All @@ -166,12 +151,17 @@ public static Member signUp(String email, String oauthId) {
.build();
}

// 신규 회원 추가 정보 등록; 닉네임/생년월일/성별
public void updateExtraInfo(String nickname, LocalDate birth, Gender gender, String introduction) {
// 신규 회원 추가 정보 등록;
public void updateExtraInfo(String nickname, LocalDate birth, Gender gender, String introduction,
String email, String password, String name, String phone) {
this.nickname = nickname;
this.birth = birth;
this.gender = gender;
this.introduction = introduction;
this.email = email;
this.password = password;
this.name = name;
this.phone = phone;
}
Comment on lines +155 to 165
Copy link

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateExtraInfo 메서드가 이제 기본 정보(email, password, name, phone)까지 업데이트하므로 메서드명이 부적절합니다. updateMemberInfo 또는 updateAllInfo와 같이 더 명확한 이름으로 변경하는 것을 권장합니다.

Copilot uses AI. Check for mistakes.

public void markAdditionalDataChecked() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.arom.with_travel.domain.member.controller;

import com.arom.with_travel.domain.member.dto.request.LocalLoginRequest;
import com.arom.with_travel.domain.member.dto.request.SignupWithSurveyRequestDto;
import com.arom.with_travel.domain.member.dto.response.LoginResponse;
import com.arom.with_travel.domain.member.service.LocalAuthService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {

private final LocalAuthService authService;

// 이메일 중복 체크
@GetMapping("/email-available")
public boolean emailAvailable(@RequestParam String email) {
return authService.isEmailAvailable(email);
}

// 이메일 등록(회원가입)
@PostMapping("/register")
public ResponseEntity<LoginResponse> register(@Valid @RequestBody SignupWithSurveyRequestDto req) {
return ResponseEntity.ok(authService.registerWithSurvey(req));
}

// 로그인
@PostMapping("/login")
public LoginResponse login(@Valid @RequestBody LocalLoginRequest req) {
return authService.login(req);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.arom.with_travel.domain.member.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class LocalLoginRequest {
@NotBlank @Email
private String email;

@NotBlank @Size(min=8, max=64)
private String password;
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ public class MemberSignupRequestDto {
private String name;

@NotBlank(message = "전화번호를 입력해주세요.")
@Pattern(regexp = "^[0-9\\-]{8,15}$", message = "전화번호 형식이 올바르지 않습니다.")
@Pattern(
regexp = "^01[0-9]-\\d{3,4}-\\d{4}$",
message = "전화번호 형식이 올바르지 않습니다. (예: 010-1234-5678)"
)
@Schema(description = "전화번호", example = "010-1234-5678")
private String phone;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.arom.with_travel.domain.member.dto.response;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class LoginResponse {
private String accessToken;
private String refreshToken;
private boolean infoChecked;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.arom.with_travel.domain.member.service;

import com.arom.with_travel.domain.member.Member;
import com.arom.with_travel.domain.member.dto.request.LocalLoginRequest;
import com.arom.with_travel.domain.member.dto.request.MemberSignupRequestDto;
import com.arom.with_travel.domain.member.dto.request.SignupWithSurveyRequestDto;
import com.arom.with_travel.domain.member.dto.response.LoginResponse;
import com.arom.with_travel.domain.member.repository.MemberRepository;
import com.arom.with_travel.domain.survey.Survey;
import com.arom.with_travel.domain.survey.dto.request.SurveyRequestDto;
import com.arom.with_travel.domain.survey.repository.SurveyRepository;
import com.arom.with_travel.global.exception.BaseException;
import com.arom.with_travel.global.exception.error.ErrorCode;
import com.arom.with_travel.global.jwt.dto.response.AuthTokenResponse;
import com.arom.with_travel.global.security.token.provider.JwtProvider;
import com.arom.with_travel.global.security.token.service.TokenService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class LocalAuthService {

private final MemberRepository memberRepository;
private final SurveyRepository surveyRepository;
private final TokenService tokenService;
private final PasswordEncoder passwordEncoder;
private final JwtProvider jwtProvider;

@Transactional(readOnly = true)
public boolean isEmailAvailable(String email) {
boolean duplicated = memberRepository.existsByEmail(email);
return !duplicated;
}

// 신규 회원 추가 정보 + 설문 통합 등록
public LoginResponse registerWithSurvey(SignupWithSurveyRequestDto req) {

String email = req.getExtraInfo().getEmail();
if(!isEmailAvailable(email)) {
throw BaseException.from(ErrorCode.DUPLICATED_EMAIL);
}

MemberSignupRequestDto extra = req.getExtraInfo();
String encodedPassword = passwordEncoder.encode(extra.getPassword());

Member member = Member.builder()
.email(extra.getEmail())
.password(encodedPassword)
.name(extra.getName())
.phone(extra.getPhone())
.birth(extra.getBirthdate())
.gender(extra.getGender())
.nickname(extra.getNickname())
.introduction(extra.getIntroduction())
.role(Member.Role.USER)
.additionalDataChecked(false)
.build();

member = memberRepository.save(member);

SurveyRequestDto s = req.getSurvey();
Survey survey = Survey.create(member, s);
surveyRepository.save(survey);
member.setSurvey(survey);

member.markAdditionalDataChecked();

AuthTokenResponse tokenPair = tokenService.issueTokenPair(member.getEmail());

return new LoginResponse(
tokenPair.getAccessToken(),
tokenPair.getRefreshToken(),
member.getAdditionalDataChecked()
);
}

private Member getUserByLoginEmailOrElseThrow(String loginEmail) {
return memberRepository.findByEmail(loginEmail)
.orElseThrow(() -> BaseException.from(ErrorCode.MEMBER_NOT_FOUND));
}

// 로그인: raw, hashed 비번 비교 → 토큰 발급
@Transactional(readOnly = true)
public LoginResponse login(LocalLoginRequest req) {
Member m = memberRepository.findByEmail(req.getEmail())
.orElseThrow(() -> BaseException.from(ErrorCode.LOGIN_FAIL));

if (m.getPassword() == null || !passwordEncoder.matches(req.getPassword(), m.getPassword())) {
throw BaseException.from(ErrorCode.LOGIN_FAIL);
}

String access = jwtProvider.generateAccessToken(m);
String refresh = jwtProvider.generateRefreshToken(m);
return new LoginResponse(access, refresh, Boolean.TRUE.equals(m.getAdditionalDataChecked()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ public MemberSignupResponseDto registerWithSurvey(String email,
Member member = getUserByLoginEmailOrElseThrow(email);

MemberSignupRequestDto extra = req.getExtraInfo();
member.updateExtraInfo(extra.getNickname(), extra.getBirthdate(), extra.getGender(), extra.getIntroduction());
member.updateExtraInfo(extra.getNickname(), extra.getBirthdate(), extra.getGender(),
extra.getIntroduction(), extra.getEmail(), extra.getPassword(),
extra.getName(), extra.getPhone());

// req.getSurveys().forEach(sdto -> {
// Survey survey = Survey.create(member, sdto.getAnswers());
Expand Down Expand Up @@ -84,7 +86,7 @@ public Member createIfNotExists(CustomOAuth2User user) {
return memberRepository.save(inserted);
});
}

private Member getUserByLoginEmailOrElseThrow(String loginEmail) {
return memberRepository.findByEmail(loginEmail)
.orElseThrow(() -> BaseException.from(ErrorCode.MEMBER_NOT_FOUND));
Expand All @@ -97,7 +99,11 @@ public MemberSignupResponseDto fillExtraInfo(String email,
member.updateExtraInfo(dto.getNickname(),
dto.getBirthdate(),
dto.getGender(),
dto.getIntroduction());
dto.getIntroduction(),
dto.getEmail(),
dto.getPassword(),
dto.getName(),
dto.getPhone());

return MemberSignupResponseDto.from(member);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
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.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
Expand Down Expand Up @@ -52,7 +53,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/api/v1/signup/**").permitAll()
.requestMatchers("/", "/index/**", "/index.js", "/favicon.ico",
"/templates", "/error", "/v3/api-docs/**", "/swagger-ui/**", "/api/v1/login",
"/actuator/**").permitAll()
"/actuator/**","/api/v1/auth/**").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling((exceptionHandling) -> exceptionHandling
Expand Down Expand Up @@ -128,6 +129,11 @@ public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public JwtFilter jwtFilter() {
return new JwtFilter(jwtProvider, memberDetailsService, securityContextRepository());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,12 @@ public enum ErrorCode {

// reply
REPLY_NOT_FOUND("REP-0000", "해당 댓글이 존재하지 않습니다.", ErrorDisplayType.POPUP),
REPLY_FORBIDDEN("REP-0001", "해당 댓글에 수정 및 삭제 권한이 없습니다.", ErrorDisplayType.POPUP)
REPLY_FORBIDDEN("REP-0001", "해당 댓글에 수정 및 삭제 권한이 없습니다.", ErrorDisplayType.POPUP),

// login
DUPLICATED_EMAIL("LOGIN-0001", "중복된 이메일이 존재합니다.", ErrorDisplayType.POPUP),
INVALID_CREDENTIALS("LOGIN-0002", "비밀번호가 올바르지 않습니다.", ErrorDisplayType.POPUP),
LOGIN_FAIL("LOGIN-0003", "로그인 과정이 정상적으로 이루어지지 않았습니다.", ErrorDisplayType.POPUP)
;

private final String code;
Expand Down