Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7b6dd04
refactor: 미션 생성시 status READY상태
cha-hyunwoo May 13, 2026
591549d
feat: ResponseEntity로 HTTP status 통일
cha-hyunwoo May 15, 2026
90074ba
fix: MissionSuccessCode CREATED 상태코드 201로 수정
cha-hyunwoo May 15, 2026
eae3b62
feat: Spring Security 의존성 추가
cha-hyunwoo May 18, 2026
bf625a0
feat: password 컬럼 추가
cha-hyunwoo May 18, 2026
d1fdf40
feat: findByEmail메서드 추가
cha-hyunwoo May 18, 2026
f4e8c49
feat: Spring Security 보안 설정 추가
cha-hyunwoo May 18, 2026
34b76bb
feat: 사용자 상세 정보 로드 서비스 추가
cha-hyunwoo May 18, 2026
be1f287
feat: Spring Security 사용자 인증 정보 클래스 추가
cha-hyunwoo May 18, 2026
89f89d7
feat: 권한이 없는 사용자가 접근할 때 JSON으로 응답해주는 핸들러 작성
cha-hyunwoo May 18, 2026
272e7a1
feat: 로그인 안했을떄 사용자가 접근할 때 JSON으로 응답해주는 핸들러 작성
cha-hyunwoo May 18, 2026
462e5ac
feat: 예외상황 핸들러 추가
cha-hyunwoo May 18, 2026
5941632
refactor: 전역 MemberSuccessCode를 Member 도메인으로 통합
cha-hyunwoo May 18, 2026
23e31fe
chore: StoreController 미사용 import 제거
cha-hyunwoo May 18, 2026
72b1311
refactor: 인증 관련 엔드포인트 수정
cha-hyunwoo May 18, 2026
1d669cf
chore: ReviewController 미사용 import 제거
cha-hyunwoo May 18, 2026
b36f639
chore: MissionController 미사용 import 제거
cha-hyunwoo May 18, 2026
2a4f073
feat: 회원가입 메서드 signUp 작성
cha-hyunwoo May 18, 2026
b115f9b
feat: 회원가입 SignUpReqDTO, SignUpResDTO 작성
cha-hyunwoo May 18, 2026
23936bd
feat: 회원가입 signUp메서드 작성
cha-hyunwoo May 18, 2026
d35a759
feat: 회원가입 toMember, toSignUp 컨버터 메서드 작성
cha-hyunwoo May 18, 2026
fa60ede
docs: 8주차 핵심키워드 작성
cha-hyunwoo May 18, 2026
e114058
feat: getUserEmail() 추가
cha-hyunwoo May 25, 2026
5f3909f
refactor: 매개변수명 username->email 변경
cha-hyunwoo May 25, 2026
d788e2b
feat: JWT authentication filter 추가
cha-hyunwoo May 25, 2026
0bd8310
feat: JWT 토큰 생성 및 검증 유틸리티 추가
cha-hyunwoo May 25, 2026
db73402
refactor: 폼 로그인 -> jwt로 변경
cha-hyunwoo May 25, 2026
cf7a1ab
feat: 로그인 api 작성
cha-hyunwoo May 25, 2026
03dbccc
feat: jwt 설정
cha-hyunwoo May 25, 2026
4af0329
feat: 커스텀 필터 생성
cha-hyunwoo May 25, 2026
fcfbf0c
feat: 커스텀 필터 생성
cha-hyunwoo May 25, 2026
5592953
feat: 마이페이지 API 개선
cha-hyunwoo May 25, 2026
50c9b0d
feat: OAuth 설정 추가
cha-hyunwoo May 25, 2026
48a7b66
docs: ch09 핵심 키워드 정리
cha-hyunwoo May 25, 2026
7f558a2
Chore: resolve merge conflicts with Hyeonu
cha-hyunwoo May 25, 2026
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
2 changes: 2 additions & 0 deletions Hyeonu/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ out/

### VS Code ###
.vscode/
# 인텔리제이 모듈 설정 파일 무시
*.xml
9 changes: 9 additions & 0 deletions Hyeonu/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ dependencies {
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

// Jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
implementation 'org.springframework.boot:spring-boot-configuration-processor'

// OAuth
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}

tasks.named('test') {
Expand Down
58 changes: 58 additions & 0 deletions Hyeonu/keyword_summary/ch09.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
- 세션과 토큰의 차이는?

| 구분 | 세션 기반 인증 | 토큰 기반 인증 |
| --- | --- | --- |
| 인증 정보 저장 위치 | 서버 |클라이언트(브라우저/앱) |
| 상태 유지 | Stateful(상태 유지) | Stateless(상태 없음) |
| 인증 방식 | 서버가 세션 ID 기억 | 토큰 자체로 사용자 인증 |
| 저장 형태 | 쿠키(JSESSIONID) | JWT 등 토큰 |
| 확장성 | 서버 늘어나면 관리 복잡 | 서버 확장 쉬움 |
| 보안 | 상대적으로 안전 | 토큰 탈취 시 위험 |

**세션은 서버가 로그인 상태를 기억하고, 토큰은 토큰 자체가 인증 정보를 가진다.**


- 엑세스 토큰과 리프레시 토큰이란?

**Access Token**

실제 인증에 사용하는 메인 토큰

- API 요청 시 사용
- Authorization 헤더에 담아 전송
- 유효기간이 짧음

**Refresh Token**

- 로그인 유지 목적
- Access Token 만료 시 새 토큰 발급
- 유효기간이 김 (2주~ 1개월)

흐름

- 로그인 성공
- Access + Refresh Token 발급
- Access Token 만료
- Refresh Token으로 재발급
- 재로그인 없이 계속 사용


- OAuth 1.0과 OAuth 2.0의 차이는?

**OAuth**

비밀번호를 직접 공유하지 않고 제 3자 서비스가 권한을 위임받는 인증 방식

예) 카카오, 구글, 네이버 로그인

| 구분 | OAuth 1.0 | OAuth 2.0 |
| --- | --- | --- |
| 보안 방식 | 복잡한 서명(Signature) | HTTPS 기반 |
| 구현 난이도 | 어려움 | 쉬움 |
| 성능 | 느림 | 빠름 |
| 사용성 | 낮음 | 높음 |
| 현재 사용 | 거의 안 씀 | 대부분 사용 |

OAuth 1.0은 서명 기반으로 복잡하고, OAuth 2.0은 토큰 기반으로 단순하고 확장성이 좋다.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OAuth 2.0을 Access Token 기반이라고만 정리하면 핵심 흐름이 다소 단순화됩니다. Authorization Code Grant 기준으로 client, resource owner, authorization server, resource server의 역할과 redirect URI, scope가 왜 필요한지까지 함께 정리하는 것을 권장합니다.


OAuth 2.0은 OAuth 1.0의 복잡한 서명 방식을 제거하고 Access Token 기반 인증을 도입하여 구현이 단순해지고 확장성이 향상된 방식이다.
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import com.example.umc10th.domain.member.service.MemberService;
import com.example.umc10th.global.apiPayload.ApiResponse;
import com.example.umc10th.domain.member.exception.code.MemberSuccessCode;
import com.example.umc10th.global.security.entity.AuthMember;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -23,11 +25,13 @@ public class MemberController {
@PostMapping("/v1/members/me")
public ResponseEntity<ApiResponse<MemberResDTO.GetInfo>> getInfo(
// 받은 JSON 데이터를 자바 객체(dto)로 변환해서 씀
@RequestBody MemberReqDTO.GetInfo dto
// @RequestBody MemberReqDTO.GetInfo dto
// 헤더에 담긴 토큰을 가지고 사용자 정보 리턴
@AuthenticationPrincipal AuthMember member
){
return ResponseEntity
.status(MemberSuccessCode.OK.getStatus())
.body(ApiResponse.onSuccess(MemberSuccessCode.OK,memberService.getInfo(dto)));
.body(ApiResponse.onSuccess(MemberSuccessCode.OK,memberService.getInfo(member)));
}

// 회원가입
Expand All @@ -39,4 +43,14 @@ public ResponseEntity<ApiResponse<MemberResDTO.SignUpResDTO>> signUp(
.status(MemberSuccessCode.SIGN_UP.getStatus())
.body(ApiResponse.onSuccess(MemberSuccessCode.SIGN_UP,memberService.signUp(dto)));
}

// 로그인
@PostMapping("/auth/login")
public ResponseEntity<ApiResponse<MemberResDTO.LoginResDTO>>login(
@RequestBody MemberReqDTO.LoginReqDTO dto
){
return ResponseEntity
.status(MemberSuccessCode.LOGIN.getStatus())
.body(ApiResponse.onSuccess(MemberSuccessCode.LOGIN,memberService.login(dto)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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.global.security.dto.OAuthDTO;

public class MemberConverter {
public static MemberResDTO.GetInfo toGetInfo(Member member) {
Expand Down Expand Up @@ -36,4 +37,20 @@ public static Member toMember(MemberReqDTO.SignUpReqDTO dto, String encodedPassw
public static MemberResDTO.SignUpResDTO toSignUpResDTO(Member member) {
return new MemberResDTO.SignUpResDTO(member.getId());
}

// 로그인
public static MemberResDTO.LoginResDTO toLoginResDTO(String token){
return MemberResDTO.LoginResDTO.builder()
.accessToken(token)
.build();
}

public static Member toMember(OAuthDTO dto) {
return Member.builder()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OAuth 회원을 Member로 변환할 때 일반 회원가입에서 필요한 birth, address, detailAddress, phoneNumber 값이 채워지지 않습니다. 현재 엔티티에서는 해당 컬럼이 nullable=false이므로 소셜 로그인 저장 시 DB 제약 조건과 충돌할 수 있습니다. 일반 회원과 소셜 회원의 필수 정보 정책을 먼저 나누고, 엔티티 제약 조건이나 추가 정보 입력 흐름을 그 정책에 맞추는 것을 권장합니다.

.email(dto.getSocialEmail())
.name(dto.getName())
.socialType(dto.getSocialType())
.socialUid(dto.getSocialUid())
Comment on lines +49 to +53
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 소셜 회원 생성 시 필수 컬럼을 채워 저장하라

문제는 MemberConverter.toMember(OAuthDTO)email/name/social만 채워서 Member를 저장한다는 점입니다. 현재 Memberbirth/address/detailAddress/phoneNumbernullable=false라서 소셜 신규 사용자의 첫 로그인 시 INSERT가 제약조건 위반으로 실패하고 인증 흐름이 끊깁니다. 소셜 가입 전용 플로우를 분리해 필수 프로필을 추가로 입력받거나, 소셜 모델에 맞게 엔티티/스키마 제약을 재설계해 저장 시점에 필수값이 항상 보장되도록 바꾸는 것이 좋습니다. 다음으로 엔티티 제약(nullable)과 가입 플로우 분리 개념을 학습해 보세요.

Useful? React with 👍 / 👎.

.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,10 @@ public record SignUpReqDTO(
String email,
String password
){}

// 로그인
public record LoginReqDTO(
String email,
String password
){}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

email 필드의 검증 조건이 없다면, swagger에서 확인했을 때 예시 값이 'string'으로 보여 email 형식으로 로그인을 강제하고 있는지 판단하기 어렵습니다. Reqeust DTO는 항상 검증 조건을 체크해야 합니다.

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,11 @@ public record GetInfo(
public record SignUpResDTO(
Long id
){}

// 로그인

@Builder
public record LoginResDTO(
String accessToken
){}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.example.umc10th.domain.member.enums.Address;
import com.example.umc10th.domain.member.enums.Gender;
import com.example.umc10th.domain.member.enums.MemberStatus;
import com.example.umc10th.domain.member.enums.SocialType;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand All @@ -27,7 +28,7 @@ public class Member extends BaseEntity {
@Column(name="name", nullable=false)
private String name;

@Column(name="gender", nullable=false)
@Column(name="gender")
@Enumerated(EnumType.STRING)
private Gender gender;

Expand Down Expand Up @@ -59,4 +60,11 @@ public class Member extends BaseEntity {
@Builder.Default
private MemberStatus status=MemberStatus.ACTIVE;

@Column(name="social_type")
@Enumerated(EnumType.STRING)
private SocialType socialType;

@Column(name="social_uid")
private String socialUid;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.umc10th.domain.member.enums;

public enum SocialType {
KAKAO
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
@RequiredArgsConstructor
public enum MemberErrorCode {
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, " MEMBER404_1","해당 사용자를 찾을 수 없습니다."),
;
INVALID_PASSWORD(HttpStatus.NOT_FOUND,"MEMBER404_2","비밀번호가 일치하지 않습니다." ),
NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.NOT_FOUND,"MEMBER404_3" ,"지원하지 않는 소셜 로그인입니다." );

private final HttpStatus status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ public enum MemberSuccessCode implements BaseSuccessCode {
"성공적으로 유저를 조회했습니다."),
SIGN_UP(HttpStatus.OK,
"MEMBER200_2",
"회원가입에 성공했습니다.");
"회원가입에 성공했습니다."),

LOGIN(HttpStatus.OK,
"MEMBER200_3",
"로그인에 성공했습니다.");


private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package com.example.umc10th.domain.member.repository;

import com.example.umc10th.domain.member.entity.Member;
import com.example.umc10th.domain.member.enums.SocialType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;


import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member,Long> {
@Query("SELECT m FROM Member m WHERE m.name=:name AND m.deletedAt IS NULL")
Optional<Member> findActiveMember(String name);

Optional<Member> findByEmail(String username);

Optional<Member> findBySocialTypeAndSocialUid(SocialType socialType, String socialUid);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import com.example.umc10th.domain.member.exception.MemberException;
import com.example.umc10th.domain.member.exception.code.MemberErrorCode;
import com.example.umc10th.domain.member.repository.MemberRepository;
import com.example.umc10th.global.security.entity.AuthMember;
import com.example.umc10th.global.security.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
Expand All @@ -16,15 +18,11 @@
public class MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
// 마이 페이지
public MemberResDTO.GetInfo getInfo(MemberReqDTO.GetInfo dto) {
// DTO에서 유저 ID를 추출
Long memberId=dto.id();
// DB에서 해당 유저 ID로 데이터 조회
Member member=memberRepository.findById(memberId)
.orElseThrow(()-> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));
public MemberResDTO.GetInfo getInfo(AuthMember member) {
// 컨버터를 이용해서 응답 DTO 생성 & return
return MemberConverter.toGetInfo(member);
return MemberConverter.toGetInfo(member.getMember());
}

// 회원가입
Expand All @@ -41,6 +39,24 @@ public MemberResDTO.SignUpResDTO signUp(MemberReqDTO.SignUpReqDTO dto) {
// Entity -> ResponseDTO 변환
return MemberConverter.toSignUpResDTO(savedMember);

}

// 로그인
public MemberResDTO.LoginResDTO login(MemberReqDTO.LoginReqDTO dto){

// 이메일로 유저 찾기
Member member=memberRepository.findByEmail(dto.email())
.orElseThrow(()->new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));

// 비밀번호 검증
if(!passwordEncoder.matches(dto.password(),member.getPassword())){
throw new MemberException(MemberErrorCode.INVALID_PASSWORD);
}

// 토큰 발급
AuthMember authMember=new AuthMember(member);
String token=jwtUtil.createAccessToken(authMember);

return MemberConverter.toLoginResDTO(token);
}
}
Loading