From fdc4ba8765b4db36278c44953840551a4b6fad86 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Sat, 16 May 2026 16:20:16 +0900 Subject: [PATCH 01/42] =?UTF-8?q?refactor=20:=20=EB=A6=AC=ED=84=B4=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/member/controller/MemberController.java | 5 ++--- .../umc10th/domain/member/converter/MemberConverter.java | 4 ++++ .../umc10th/domain/mission/controller/MissionController.java | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index c61e09c2..a1ebdeda 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -39,11 +39,10 @@ public ResponseEntity> getHome( @AuthenticationPrincipal Long memberId, @RequestParam(defaultValue = "0") int page ){ - BaseSuccessCode code = MemberSuccessCode.OK; MemberResDTO.HomeResultDto result = memberService.getHome(memberId, page); return ResponseEntity - .status(code.getStatus()) - .body(ApiResponse.onSuccess(code, result)); + .status(MemberSuccessCode.OK.getStatus()) + .body(ApiResponse.onSuccess(MemberSuccessCode.OK, result)); } // 진행중/완료 미션 목록 조회 diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index a7412b8f..e4af073a 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -6,7 +6,11 @@ import com.example.umc10th.domain.member.entity.mapping.MemberMission; import org.springframework.data.domain.Page; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import java.util.Collection; import java.util.List; import java.util.stream.Collectors; diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index b784e565..240ba037 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -26,7 +26,6 @@ public class MissionController { private final MissionService missionService; - //리뷰 작성 (완료된 미션에 한해서) @PostMapping("v1/missions/{missionId}/reviews") public ResponseEntity> writeReview( From 1e35650df1e1613523f0c5eac6442a37fe9e3dff Mon Sep 17 00:00:00 2001 From: Gibeom Date: Sat, 16 May 2026 16:20:43 +0900 Subject: [PATCH 02/42] =?UTF-8?q?feat=20:=20=EC=9D=98=EC=A1=B4=EC=84=B1=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 --- Gibeom/umc10th/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Gibeom/umc10th/build.gradle b/Gibeom/umc10th/build.gradle index 2314bead..183ef1c7 100644 --- a/Gibeom/umc10th/build.gradle +++ b/Gibeom/umc10th/build.gradle @@ -31,6 +31,10 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testAnnotationProcessor 'org.projectlombok:lombok' + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1' From 1c599ee4029f7175464f67d0a3bd66fd320e6040 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Sat, 16 May 2026 16:21:09 +0900 Subject: [PATCH 03/42] =?UTF-8?q?feat=20:=20SecurityConfig=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 --- .../umc10th/global/config/SecurityConfig.java | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 6b8c1df0..13748aa5 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -5,19 +5,49 @@ 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; @Configuration @EnableWebSecurity 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(auth -> auth - .anyRequest().permitAll() + .csrf(AbstractHttpConfigurer::disable) // csrf 방어기능 끄기 + // 위의 허용한 경로로 요청이 들어오면 모두 허용 + .authorizeHttpRequests(requests -> requests + .requestMatchers(allowUris).permitAll() + .anyRequest().authenticated() + ) + //스프링 시큐리티가 제공하는 기본 로그인 폼을 사용 + .formLogin(form -> form + .defaultSuccessUrl("/swagger-ui.html", true) + .permitAll() + //로그아웃 주소로 요청을 보내면 로그아웃 처리를 진행 + ).logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") // 로그아웃 성공 시 로그인 페이지로 리다이렉트 + .permitAll() ); return http.build(); } -} + + //해시 알고리즘을 이용해 함호화된 Bcrypt 알고리즘 객체 반환 + //알고리즘은 실행때마다 매번 다른 결과물 생성 + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file From 397f3a9518aa46b49c295049cbc8b45df9eb2553 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:30:49 +0900 Subject: [PATCH 04/42] =?UTF-8?q?refactor=20:=20=EC=95=88=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20=EC=97=90=EB=9F=AC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/feedback_summary/FeedBack_Study | 26 ------------------- .../keyword_summary/{ch07.md => ch08.md} | 0 .../apiPayload/code/GeneralErrorCode.java | 7 ++--- 3 files changed, 4 insertions(+), 29 deletions(-) delete mode 100644 Gibeom/umc10th/feedback_summary/FeedBack_Study rename Gibeom/umc10th/keyword_summary/{ch07.md => ch08.md} (100%) diff --git a/Gibeom/umc10th/feedback_summary/FeedBack_Study b/Gibeom/umc10th/feedback_summary/FeedBack_Study deleted file mode 100644 index 04d93ed2..00000000 --- a/Gibeom/umc10th/feedback_summary/FeedBack_Study +++ /dev/null @@ -1,26 +0,0 @@ -ResponseEntity -- 정의 : 스프링 프레임워크에서 제공하는 클래스. -- Http요청 또는 응답에 해당하는 HttpStatus, HttpHeader와 HttpBody를 포함하는 클래스 -- Http 응답의 주권을 가짐. 커스텀 가능 -- 상속 구현 클래스 (RequestBody, ResponseEntity) - -기존 상태 -- 클라이언트 측에서는 ApiResponse로 응답시 정해진 응답 (예 : 200이면 200, 500이면 500) -- 현재 ApiResponse 클래스는 정적 팩토리 메서드 형식. -- ApiResponse.java - ``` - //성공한 경우 - public static ApiResponse onSuccess(BaseSuccessCode code, T result){ - return new ApiResponse<>(true, code.getCode(), code.getMessage(), result); - } - ``` -기존 방식의 한계 -- 클라이언트 측은 먼저 http status를 확인하고 바디를 확인함 -- 네트워크 불일치 : ApiResponse만 반환하면 내부 로직이 실패하더라도 Http 상태코드는 기본(200)으로 나가는 경우가 많았음 -- 상세한 정보가 Json바디 안에 숨겨져 있어 클라이언트가 Http 표준방식으로 1차 판단을 내리기 어려웠음 -- 단순한 소통 : 200, 500 위주의 단순한 소통만 가능했고, 201, 204 등 을 활용 못했음 - -Why? -- API 사용자(프론트엔드 등등)는 응답바디 안의 Json뿐 아닌 브라우저나 앱이 수신하는 Status Code를 보고 1차 판단을 내림. -- 유연성 : ResponseEntity를 래핑해 감싼 응답을 보낸다면 커스텀 응답, 쿠키, 헤더 등 표준화된 응답을 유연하게 보낼 수 있음. -- ApiResponse의 데이터 규격을 유지하고 ResponseEntity로 감싸 네트워크 수준 상태코드로 명시적 관리 \ No newline at end of file diff --git a/Gibeom/umc10th/keyword_summary/ch07.md b/Gibeom/umc10th/keyword_summary/ch08.md similarity index 100% rename from Gibeom/umc10th/keyword_summary/ch07.md rename to Gibeom/umc10th/keyword_summary/ch08.md diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java index b05e24f2..87c93a9c 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java @@ -10,9 +10,6 @@ public enum GeneralErrorCode implements BaseErrorCode{ BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400_1", "잘못된 요청입니다."), - UNAUTHORTIZED(HttpStatus.UNAUTHORIZED, - "COMMON401_1", - "인증되지 않았습니다."), FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403_1", "접근이 금지되었습니다."), @@ -29,6 +26,10 @@ public enum GeneralErrorCode implements BaseErrorCode{ "COMMON500", "서버 내부 오류입니다." ), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, + "COMMON401_2", + "승인되지 않았습니다." + ) ; private final HttpStatus status; private final String code; From b90c45f6f4efa543ad60909020c634ab78cffaed Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:31:42 +0900 Subject: [PATCH 05/42] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=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 --- .../member/controller/MemberController.java | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index a1ebdeda..941d296e 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -1,5 +1,6 @@ package com.example.umc10th.domain.member.controller; +import com.example.umc10th.domain.member.dto.MemberReqDTO; import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.enums.MissionStatus; import com.example.umc10th.domain.member.service.MemberService; @@ -7,8 +8,9 @@ import com.example.umc10th.global.apiPayload.ApiResponse; import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; +import com.example.umc10th.global.entity.AuthMember; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -17,12 +19,12 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/v1/members") +@RequestMapping("/api") public class MemberController { private final MemberService memberService; //마이페이지 - @GetMapping("/me") + @GetMapping("/v1/members/me") public ResponseEntity> getInfo( @AuthenticationPrincipal Long memberId ){ @@ -34,19 +36,19 @@ public ResponseEntity> getInfo( } // 홈화면 - @GetMapping("/home") + @GetMapping("/v1/members/home") public ResponseEntity> getHome( - @AuthenticationPrincipal Long memberId, + @AuthenticationPrincipal AuthMember authMember, @RequestParam(defaultValue = "0") int page ){ - MemberResDTO.HomeResultDto result = memberService.getHome(memberId, page); + MemberResDTO.HomeResultDto result = memberService.getHome(authMember.getMember().getId(), page); return ResponseEntity .status(MemberSuccessCode.OK.getStatus()) .body(ApiResponse.onSuccess(MemberSuccessCode.OK, result)); } // 진행중/완료 미션 목록 조회 - @GetMapping("/missions") + @GetMapping("/v1/members/missions") public ResponseEntity>> getMissionsByStatus( @AuthenticationPrincipal Long memberId, @RequestParam MissionStatus status, @@ -61,4 +63,15 @@ public ResponseEntity>> getMissionsBy .body(ApiResponse.onSuccess(code, result)); } + + //회원가입 + @PostMapping("/auth/sign-up") + public ResponseEntity> signUp( + @RequestBody @Valid MemberReqDTO.SignUp req + ){ + memberService.signUp(req); + return ResponseEntity + .status(MemberSuccessCode.CREATED.getStatus()) + .body(ApiResponse.onSuccess(MemberSuccessCode.CREATED, null)); + } } From 487ba8ea60d2090515b75607e9de193be2c6eee7 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:32:13 +0900 Subject: [PATCH 06/42] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/service/MemberService.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index 70b5aae5..0d9b76f1 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -17,6 +17,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.List; @@ -27,6 +28,7 @@ public class MemberService { private final MemberRepository memberRepository; private final MemberMissionRepository memberMissionRepository; + private final PasswordEncoder passwordEncoder; public MemberResDTO.GetInfo getInfo(Long memberId) { Member member = memberRepository.findById(memberId) @@ -65,4 +67,17 @@ public List getMissionsByStatus( .collect(Collectors.toList()); return MissionConverter.toMissionDtoList(missions); } + + public void signUp(MemberReqDTO.SignUp req) { + //닉네임 혹은 이메일이 이미 존재할 때 + if(memberRepository.existsByEmail(req.email())){ + throw new MemberException(MemberErrorCode.EMAIL_DUPLICATED); + } else if (memberRepository.existsByNickname(req.nickname())){ + throw new MemberException(MemberErrorCode.NICKNAME_DUPLICATED); + } + + String encodedPassword = passwordEncoder.encode(req.password()); + Member member = MemberConverter.toMember(req, encodedPassword); + memberRepository.save(member); + } } From 9beb252e4865bde78a0e21bae3c0a1acec478106 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:32:39 +0900 Subject: [PATCH 07/42] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=BB=A8=EB=B2=84=ED=84=B0=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/converter/MemberConverter.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index e4af073a..e1297d29 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -48,4 +48,16 @@ public static MemberResDTO.HomeMissionDto toHomeMissionDto(MemberMission memberM .status(memberMission.getStatus().name()) .build(); } + + public static Member toMember(MemberReqDTO.SignUp req, String emcodedPasssword){ + return Member.builder() + .name(req.name()) + .password(emcodedPasssword) + .phoneNumber(req.phoneNumber()) + .email(req.email()) + .gender(req.gender()) + .userPoint(0) + .nickname(req.nickname()) + .build(); + } } From 8627cd53c6b96bb8e4d4d7d0251e55b23b94d5cf Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:33:02 +0900 Subject: [PATCH 08/42] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20DTO=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/dto/MemberReqDTO.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java index 943a141f..047e254a 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java @@ -1,5 +1,24 @@ package com.example.umc10th.domain.member.dto; +import com.example.umc10th.domain.member.enums.Gender; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + public class MemberReqDTO { + public record SignUp ( + @NotBlank + String name, + @NotBlank + String nickname, + @Email @NotBlank + String email, + @NotBlank + String password, + @NotBlank + String phoneNumber, + Gender gender + ){} + } From f2bd85af51eabe0593fa900656e1658e15fc2d65 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:33:50 +0900 Subject: [PATCH 09/42] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20Boolean=20=EB=A9=94=EC=86=8C=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 --- .../umc10th/domain/member/repository/MemberRepository.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index 054ad34f..9813a157 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -3,5 +3,10 @@ import com.example.umc10th.domain.member.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface MemberRepository extends JpaRepository { + Boolean existsByEmail(String email); + Optional findByEmail(String email); + Boolean existsByNickname(String nickname); } From 58b0f081e9d5ab5af677ac8c8531ef8219e9ad81 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:52:14 +0900 Subject: [PATCH 10/42] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=84=B1=EA=B3=B5=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/example/umc10th/domain/member/entity/Member.java | 3 +++ .../domain/member/exception/code/MemberSuccessCode.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java index 1f7fca6b..51df7d92 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -46,6 +46,9 @@ public class Member extends BaseEntity{ @Column(name = "email") private String email; + @Column(name = "password", nullable = false) + private String password; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "region_id") private Region region; diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java index 5a5c3362..2d76db0b 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java @@ -11,7 +11,7 @@ public enum MemberSuccessCode implements BaseSuccessCode { "MEMBER200_1", "성공적으로 유저를 조회했습니다."), - SIGNUP_SUCCESS(HttpStatus.CREATED, + CREATED(HttpStatus.CREATED, "MEMBER_201_1", "회원가입이 성공적으로 완료되었습니다.") ; From c27c02e27356cab68b7d066885b5196db8e1d627 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:53:14 +0900 Subject: [PATCH 11/42] =?UTF-8?q?feat=20:=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=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/exception/code/MemberErrorCode.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index 98c24940..3ffc9cef 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -12,7 +12,15 @@ public enum MemberErrorCode implements BaseErrorCode { "COMMON404_1", "해당 사용자를 찾을 수 없습니다." ), + EMAIL_DUPLICATED(HttpStatus.NOT_FOUND, + "COMMON404_2", + "해당 사용자를 찾을 수 없습니다." + ), + NICKNAME_DUPLICATED(HttpStatus.CONFLICT, + "COMMON409_2", + "이미 존재하는 닉네임입니다." + ) ; private final HttpStatus status; private final String code; From 88d152554afacd2dff2e7b2e3e5c8d8ffd47f728 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:53:49 +0900 Subject: [PATCH 12/42] =?UTF-8?q?feat=20:=20SecurityConfig=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/config/SecurityConfig.java | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 13748aa5..d28d0edf 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -1,10 +1,13 @@ package com.example.umc10th.global.config; +import com.example.umc10th.global.security.CustomAccessDenied; +import com.example.umc10th.global.security.CustomEntryPoint; 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.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @@ -15,32 +18,36 @@ public class SecurityConfig { private final String[] allowUris = { //swagger 허용 // 자유롭게 이용할 수 있는 주소 (비로그인) - "swagger-ui/**", + "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**", //로그인 - "/auth/**", + "/api/auth/**", }; @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain SecurityFilterChain(HttpSecurity http) throws Exception { http - .csrf(AbstractHttpConfigurer::disable) // csrf 방어기능 끄기 - // 위의 허용한 경로로 요청이 들어오면 모두 허용 - .authorizeHttpRequests(requests -> requests + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(requests ->requests .requestMatchers(allowUris).permitAll() .anyRequest().authenticated() ) - //스프링 시큐리티가 제공하는 기본 로그인 폼을 사용 .formLogin(form -> form - .defaultSuccessUrl("/swagger-ui.html", true) + .defaultSuccessUrl("/swagger-ui/index.html", true) .permitAll() - //로그아웃 주소로 요청을 보내면 로그아웃 처리를 진행 - ).logout(logout -> logout + ) + .logout(logout -> logout .logoutUrl("/logout") - .logoutSuccessUrl("/login?logout") // 로그아웃 성공 시 로그인 페이지로 리다이렉트 + .logoutSuccessUrl("/login?logout") .permitAll() - ); + ) + //예외 상황 핸들러 + .exceptionHandling(exception -> exception + .accessDeniedHandler(customAccessDenied()) + .authenticationEntryPoint(customEntryPoint()) + ) + ; return http.build(); } @@ -50,4 +57,14 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } -} \ No newline at end of file + + @Bean + public CustomAccessDenied customAccessDenied() { + return new CustomAccessDenied(); + } + + @Bean + public CustomEntryPoint customEntryPoint() { + return new CustomEntryPoint(); + } +} From 288d256be497232adc496ae05d98db39ad51ba46 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 19:54:16 +0900 Subject: [PATCH 13/42] =?UTF-8?q?docs=20:=208=EC=A3=BC=EC=B0=A8=20?= =?UTF-8?q?=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gibeom/umc10th/keyword_summary/ch08.md | 102 +++++++------------------ 1 file changed, 27 insertions(+), 75 deletions(-) diff --git a/Gibeom/umc10th/keyword_summary/ch08.md b/Gibeom/umc10th/keyword_summary/ch08.md index 701095b1..737accb1 100644 --- a/Gibeom/umc10th/keyword_summary/ch08.md +++ b/Gibeom/umc10th/keyword_summary/ch08.md @@ -1,90 +1,42 @@ -- Page와 Slice +- Spring Security가 무엇인가? - Spring Data JPA는 페이징을 위해 두가지 객체를 제공한다. Slice & Page + 스프링 기반 애플리케이션의 보안을 담당하는 강력하고 포괄적인 하위 프레임워크 - DB에 저장된 데이터들을 페이지에 맞춰 몇개씩 뿌릴 건지 알려주는 것 + 복잡한 보안 로직을 직접 구현할 필요 없이 표준화된 필터 기반의 설정을 통해 시스템을 안전하게 보호한다. - - Page - - pageable 객체를 사용해 오프셋 기반 페이지네이션을 구현할 수 있는 객체. - - Slice를 상속함 - - Slice - - Page 보다 좀 더 추상적. - - 커서 기반 페이지네이션을 구현할 때 사용할 수 있는 객체 - - Response에 총 데이터 갯수는 보내지 않음 - - Pageable 객체 - - JPA가 제공하는 페이지네이션을 위한 객체 - - 페이징 정보(페이지 번호, 페이지 크기, 정렬방식 등..)를 담고 있는 인터페이스 - - 스프링 Data JPA에서 제공하는 `PageRequest` 클래스를 통해 인스턴스화 가능 - - 사용 이유 - - 모든 데이터를 한번에 뿌리게 되면 성능 저하 등 문제가 발생 - - 페이지네이션을 사용해 적절한 수의 정보만 올려주면 프론트에서 적절한 처리가능. -- Java stream API +- 인증(Authentication)vs 인가(Authorization) - 자바가 지원하는 함수형 프로그래밍 방식. + 비슷해보이지만 서로 다른 개념이다. - 함수형 프로그래밍이란? + 인증 (Authentication) - - 코드가 “어떻게” 에서 “무엇을”로 포커싱이 옮겨간 프로그래밍 방식 - - 함수형 프로그래밍에서 함수는 `Int` ,`String` 같은 1급 객체로 취급. - - 이 특성을 활용하는 고차함수 : map, filter, reduce 등… - - (기존 객체의 상태를 변경하지 않고, 새로운 결과를 반환) - - 사용 이유 : 가독성, 불변성, 유지보수성, 병렬 처리성 (쓰레드 풀) + - 본인확인 절차 + - 사용자가 자신이 주장하는 사람이 맞는지 확인하는 과정 - 단점 + 인가 (Authorization) - - for문 같은 단순 반복문에 익숙한 개발자들에게는 람다식과 스트림이 이해가 어려울 수 있음 - - 중단점을 걸어서 디버깅하기 어려움. - - for문 보다 오버헤드가 조금 더 큼. + - 권한확인 절차 + - 인증된 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 과정 - 병렬 처리 +- Stateful vs Stateless - - 작업을 여러 스레드에게 분할해 병렬적으로 처리하는 방법 - - `parallelStram()` 키워드와 `ForkJoinPool()` 을 사용해 스레드 지정 가능 - - CommonPool : 미리 생성해 놓은 스레드를 모든 애플리케이션이 이용하는 방식의 풀 - - 장점 : 정해진 수의 스레드를 미리 생성해 놓고 사용하기 때문에 스레드 생성/삭제 오버헤드 적음. 적은 양의 처리에 유리 - - 단점 : 각각의 애플리케이션 마다 알맞은 처리를 제공하기 힘들 수 있음 - - (군대 배식 느낌) - - ThreadPool : 각각의 컴포넌트마다 설정할 수 있는 풀 - - 장점 : 성능 튜닝 가능 - - 메모리 관리 등 신경써야 할 것들이 있음. - - (오마카세 느낌) -- 객체 그래프 탐색 + 논점 : 서버가 클라이언트의 세션 정보를 기억하는가? - 객체 그래프 탐색 (Object Graph Navigation) + Stateful(상태유지) : 세션 정보를 기억함 - - 객체지향언어에서 참조를 사용해 연관된 객체를 타고 들어가 데이터를 조회하는 방식 - - ex) 멤버 객체에서 member.getTeam()처럼 연관관계를 통해 팀 정보를 가져오는 방식 + Stateless(토큰 기반) : 서버가 상태를 유지하지 않으므로 요청에 포함된 토큰(JWT)로 검증 - 특징 : 객체 간 연관관계를 통해 자유롭게 메모리상의 객체를 이동하며 탐색 + | 구분 | Stateful | Stateless | + | --- | --- | --- | + | 특징 | 서버가 세션 저장소에 로그인 상태 유지 | 서버가 상태를 유지하지 않음, 요청에 포함된 토큰으로 검증 | + | 인증방식 | JSESSION쿠키를 통해 서버 메모리/DB의 세션 조회 | 매 요청시 HTTP헤더에 토큰을 담아서 전송 (Authorization:Bearer) | + | 서버 확장 | 세션 불일치 문제 발생 가능 | 각 요청이 독립적이므로 서버 증설에 유리 | + | 메모리 및 비용 | 동시접속자가 많을수록 서버 세션 메모리 소비 증가 | 토큰 검증 연산이 필요하며, 서버 메모리 사용량은 적음 | + | 주요 활용처 | 전톤적인 웹 애플리케이션 | REST API, 모바일 앱, MSA | - 장점 : SQL 직접 작성 시 필요한 조인 제약에서 벗어나 논리적인 도메인 모델 구조에 따라 데이터 조회 가능 + 서버 확장 방법 - JPA에서의 활용 : JPA는 연관된 객체를 처음부터 로딩하지 않고 실제 사용 시점에 조회하는 지연로딩을 사용해 객체그래프 탐색을 지원 - - 주의점 : 객체 그래프를 무분별 탐색하는 경우, 하이버네이트 등에서 N+1 문제가 발생할 수 있음. - - **내 요약** - - - Store store = member.getReviewList().get(0).getStore()와 같은 코드가 있다고 가정. - 1. 멤버의 리뷰리스트를 가져오고 - 2. 0번 객체를 가져오고 - 3. 그 리뷰의 가게를 가져오는 흐름을 표현 가능. - - 여기서 문제 : 어디까지 조회가 가능한가? - - 실행시 모든 데이터를 올려둘 수는 없음. 그렇다면 사용할 때만 필요한 정보를 가져오는 방식 Lazy-Loading(지연로딩)을 사용해 `.` 을 사용해 다음 객체를 불러올때마다 데이터를 가져옴. - - 만약 객체간 연결이 없거나, 다음 데이터를 조회할 수 없다면 null이 조회되거나 오류가 날 것. -- @Valid vs @Validated - - @Valid - - - 컨트롤러 단에서 객체의 유효성을 검사할 때 사용 - - 계층 구조 검증이 가능 (User 안에 있는 Address 객체도 검사하고 싶으면 Address 필드 위에 @Valid 를 붙여야 함) - - 한계 : 그룹화 검증이 불가능 - - @Validated - - - 스프링에서 자바표준기능을 확장해 만든 어노테이션 - - 용도 : Service 나 Repository 등 Bean 계층에서 검증이 필요할 때, 혹은 그룹검증이 필요할 때 사용. - - 특징 - - 제약조건 그룹화 : “회원가입때는 이 필드 검사하고, 정보수정때는 하지마” 같은 상황을 `groups` 속성으로 지정가능 - - 클래스 레벨 선언 : 클래스 상단에 `@Validated` 어노테이션을 붙여야 해당 클래스 내부의 메서드 파라미터에 대한 검증이 작동 - - 한계 : 계층구조 검증은 직접적으로 지원하지 않아, 내부 객체에는 여전히 @Valid를 써야함. \ No newline at end of file + - Scale-up : 단일 서버 성능 향상 + - Scale-out : 서버의 개수를 늘리기 + - 로드밸런서 : 서버 부하를 분산시키는 H/W, S/W + - 클라이언트와 서버Pool 사이에 위치해 서버의 부하를 분산시키는 하드웨어나 S/W \ No newline at end of file From 380bc8d6323d0369bd6d2c4f6ca8128fb9e870bf Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 20:01:32 +0900 Subject: [PATCH 14/42] =?UTF-8?q?feat=20:=20AuthMember=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 --- .../umc10th/global/entity/AuthMember.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/entity/AuthMember.java diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/entity/AuthMember.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/entity/AuthMember.java new file mode 100644 index 00000000..0a9e8f40 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/entity/AuthMember.java @@ -0,0 +1,29 @@ +package com.example.umc10th.global.entity; + +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.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() { + return List.of(); + } + @Override + public String getPassword() { + return member.getPassword(); + } + @Override + public String getUsername() { + return member.getEmail(); + } +} From bcee1aea7c2622e47eb8c9faae6a08fcb9951dd5 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 20:02:23 +0900 Subject: [PATCH 15/42] =?UTF-8?q?feat=20:=20=EC=9C=A0=EC=A0=80=20=EB=94=94?= =?UTF-8?q?=ED=85=8C=EC=9D=BC=EC=9D=84=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EB=8A=94=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=A0=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CustomUserDetailsService.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/service/CustomUserDetailsService.java diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/service/CustomUserDetailsService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/service/CustomUserDetailsService.java new file mode 100644 index 00000000..173be810 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/service/CustomUserDetailsService.java @@ -0,0 +1,27 @@ +package com.example.umc10th.global.service; + +import com.example.umc10th.domain.member.entity.Member; +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.entity.AuthMember; +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 username + ) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + return new AuthMember(member); + } +} From 8ad2b46ebbd47b13333c4c34c53ee2d64d346849 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 19 May 2026 20:02:57 +0900 Subject: [PATCH 16/42] =?UTF-8?q?feat=20:=20=EC=9D=B8=EC=A6=9D,=20?= =?UTF-8?q?=EC=9D=B8=EA=B0=80=20=EC=98=A4=EB=A5=98=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/CustomAccessDenied.java | 21 +++++++++++++++++++ .../global/security/CustomEntryPoint.java | 21 +++++++++++++++++++ .../global/security/SecurityResponseUtil.java | 19 +++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/SecurityResponseUtil.java diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java new file mode 100644 index 00000000..fc4ad57b --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java @@ -0,0 +1,21 @@ +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 org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; + +import java.io.IOException; + +public class CustomAccessDenied implements AccessDeniedHandler { + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException { + SecurityResponseUtil.writeErrorResponse(response, GeneralErrorCode.FORBIDDEN); + } +} \ No newline at end of file diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java new file mode 100644 index 00000000..60e7713b --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java @@ -0,0 +1,21 @@ +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 org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import java.io.IOException; + +public class CustomEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException { + SecurityResponseUtil.writeErrorResponse(response, GeneralErrorCode.UNAUTHORIZED); + } +} diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/SecurityResponseUtil.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/SecurityResponseUtil.java new file mode 100644 index 00000000..251ca496 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/SecurityResponseUtil.java @@ -0,0 +1,19 @@ +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 java.io.IOException; + +public class SecurityResponseUtil { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static void writeErrorResponse(HttpServletResponse response, BaseErrorCode code) throws IOException { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + objectMapper.writeValue(response.getOutputStream(), ApiResponse.onFailure(code, null)); + } +} From 4677dbf399376d7aae70dd8cd2c684cee5bb20f4 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Wed, 20 May 2026 14:19:38 +0900 Subject: [PATCH 17/42] =?UTF-8?q?refactor=20:=20=EC=98=A4=ED=83=80=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 --- .../member/converter/MemberConverter.java | 4 +- .../exception/code/MemberErrorCode.java | 2 +- .../review/controller/ReviewController.java | 2 +- .../domain/review/service/ReviewService.java | 78 ++++++++----------- 4 files changed, 35 insertions(+), 51 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index e1297d29..e1b3c6bf 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -49,10 +49,10 @@ public static MemberResDTO.HomeMissionDto toHomeMissionDto(MemberMission memberM .build(); } - public static Member toMember(MemberReqDTO.SignUp req, String emcodedPasssword){ + public static Member toMember(MemberReqDTO.SignUp req, String encodedPasssword){ return Member.builder() .name(req.name()) - .password(emcodedPasssword) + .password(encodedPasssword) .phoneNumber(req.phoneNumber()) .email(req.email()) .gender(req.gender()) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index 3ffc9cef..84282c58 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -12,7 +12,7 @@ public enum MemberErrorCode implements BaseErrorCode { "COMMON404_1", "해당 사용자를 찾을 수 없습니다." ), - EMAIL_DUPLICATED(HttpStatus.NOT_FOUND, + EMAIL_DUPLICATED(HttpStatus.CONFLICT, "COMMON404_2", "해당 사용자를 찾을 수 없습니다." ), diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index e78133cb..bcdfabaa 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -30,7 +30,7 @@ public ResponseEntity result = reviewService.getMemberReviewsOrderByScore(memberId, pageSize, cursor, query); + ReviewResDTO.Pagination result = reviewService.getMemberReviews(memberId, pageSize, cursor, query); return ResponseEntity .status(code.getStatus()) .body(ApiResponse.onSuccess(code, result)); diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java index f6c18235..8870cd00 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java @@ -7,10 +7,8 @@ import com.example.umc10th.domain.review.exception.code.ReviewErrorCode; import com.example.umc10th.domain.review.repository.ReviewRepository; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import java.util.List; @@ -20,40 +18,24 @@ public class ReviewService { private final ReviewRepository reviewRepository; //id 페이지네이션 - public ReviewResDTO.Pagination getMemberReviewsOrderById( + private ReviewResDTO.Pagination getMemberReviewsOrderById( Long memberId, Integer pageSize, - String cursor, - String query + String cursor ) { - - //페이지 정보 PageRequest만들기 PageRequest pageRequest = PageRequest.of(0, pageSize); - long idCursor; Slice reviewList; String nextCursor; - //커서가 있는 경우 if (!cursor.equals("-1")) { - - // 커서 분리 - String[] cursorSplit = cursor.split(":"); - switch (query.toLowerCase()) { - case "id": - idCursor = Long.parseLong(cursorSplit[1]); - // 멤버의 리뷰들 조회 & where 절에 커서값 기입 - reviewList = reviewRepository.findReviewsByMember_IdAndIdLessThanOrderByIdDesc( - memberId, - idCursor, - pageRequest - ); - break; - default: - throw new ReviewException(ReviewErrorCode.QUERY_NOT_VALID); - } + long idCursor = Long.parseLong(cursor.split(":")[1]); + reviewList = reviewRepository.findReviewsByMember_IdAndIdLessThanOrderByIdDesc( + memberId, + idCursor, + pageRequest + ); } else { - //커서 없이 조회 reviewList = reviewRepository.findReviewsByMember_IdOrderByIdDesc(memberId, pageRequest); } @@ -70,38 +52,40 @@ public ReviewResDTO.Pagination getMemberReviewsOrderById reviewList.getSize() ); } - //별점 순 페이징 - public ReviewResDTO.Pagination getMemberReviewsOrderByScore( + // query 값에 따라 정렬 전략 선택 — 컨트롤러는 이 메서드만 호출한다 + public ReviewResDTO.Pagination getMemberReviews( Long memberId, Integer pageSize, String cursor, String query + ) { + return switch (query.toLowerCase()) { + case "id" -> getMemberReviewsOrderById(memberId, pageSize, cursor); + case "score" -> getMemberReviewsOrderByScore(memberId, pageSize, cursor); + default -> throw new ReviewException(ReviewErrorCode.QUERY_NOT_VALID); + }; + } + + //별점 순 페이징 + private ReviewResDTO.Pagination getMemberReviewsOrderByScore( + Long memberId, + Integer pageSize, + String cursor ) { PageRequest pageRequest = PageRequest.of(0, pageSize); Slice reviewList; String nextCursor; - //커서가 있는 경우 + if (!cursor.equals("-1")) { - // 커서 분리 String[] cursorSplit = cursor.split(":"); - switch (query.toLowerCase()) { - case "score": - // 커서 타입 변환 - long scoreCursor = Long.parseLong(cursorSplit[0]); - long idCursor = Long.parseLong(cursorSplit[1]); - - //리뷰들 조회 & where절에 커서 값 기입 - reviewList = reviewRepository.findReviewsByScoreCursor( - memberId, - scoreCursor, - idCursor, - pageRequest); - break; - default: - throw new ReviewException(ReviewErrorCode.QUERY_NOT_VALID); - } + long scoreCursor = Long.parseLong(cursorSplit[0]); + long idCursor = Long.parseLong(cursorSplit[1]); + reviewList = reviewRepository.findReviewsByScoreCursor( + memberId, + scoreCursor, + idCursor, + pageRequest); } else { - //커서 없이 조회 reviewList = reviewRepository.findReviewsByMember_IdOrderByScoreDescIdDesc(memberId, pageRequest); } List content = reviewList.getContent(); From e4640d2098ec6f5b54e169f9f7d8c9e7c5ba721e Mon Sep 17 00:00:00 2001 From: Gibeom Date: Fri, 22 May 2026 13:27:37 +0900 Subject: [PATCH 18/42] =?UTF-8?q?chore:=20.env=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=B6=94=EC=A0=81=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gibeom/umc10th/.env | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 Gibeom/umc10th/.env diff --git a/Gibeom/umc10th/.env b/Gibeom/umc10th/.env deleted file mode 100644 index 43b455e5..00000000 --- a/Gibeom/umc10th/.env +++ /dev/null @@ -1,3 +0,0 @@ -DB_USER=USER -DB_PW=MY_PASSWORD_HERE -DB_URL=MY_URL \ No newline at end of file From 7af73d215fa7f93d867491625797e8bd06887c27 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Fri, 22 May 2026 13:28:21 +0900 Subject: [PATCH 19/42] =?UTF-8?q?feat=20:=20build.gradle=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 --- Gibeom/umc10th/build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Gibeom/umc10th/build.gradle b/Gibeom/umc10th/build.gradle index 183ef1c7..53339b49 100644 --- a/Gibeom/umc10th/build.gradle +++ b/Gibeom/umc10th/build.gradle @@ -22,6 +22,13 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-webmvc' implementation 'org.springframework.boot:spring-boot-starter-security' + + // 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' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' From 5fcaa5a8851e6cff871804aa6abd9e0aaf1b4945 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Fri, 22 May 2026 13:38:52 +0900 Subject: [PATCH 20/42] =?UTF-8?q?feat=20:=20JWT=20=EA=B4=80=EB=A0=A8=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 --- Gibeom/umc10th/src/main/resources/application.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Gibeom/umc10th/src/main/resources/application.yml b/Gibeom/umc10th/src/main/resources/application.yml index f7681ea6..62eed079 100644 --- a/Gibeom/umc10th/src/main/resources/application.yml +++ b/Gibeom/umc10th/src/main/resources/application.yml @@ -16,4 +16,10 @@ spring: ddl-auto: update # ?????? ?? ? ?????? ???? ??? ?? properties: hibernate: - format_sql: true # ???? SQL ??? ?? ?? ??? \ No newline at end of file + format_sql: true # ???? SQL ??? ?? ?? ??? + +jwt: + token: + secretKey: ${JWT_SECRET_KEY} + expiration: + access: 1800000 # 30분 \ No newline at end of file From ca7c6f0224905525bbac5fbc69cb700978440690 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Fri, 22 May 2026 13:40:03 +0900 Subject: [PATCH 21/42] =?UTF-8?q?feat=20:=20=EB=B3=B5=ED=98=B8=ED=99=94=20?= =?UTF-8?q?Util=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/umc10th/global/util/JwtUtil.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/util/JwtUtil.java diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/util/JwtUtil.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/util/JwtUtil.java new file mode 100644 index 00000000..48c2c1c5 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/util/JwtUtil.java @@ -0,0 +1,93 @@ +package com.example.umc10th.global.util; + +import com.example.umc10th.global.entity.AuthMember; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.stream.Collectors; + +@Component +public class JwtUtil { + + private final SecretKey secretKey; + private final Duration accessExpiration; + + public JwtUtil( + @Value("${jwt.token.secretKey}") String secret, + @Value("${jwt.token.expiration.access}") Long accessExpiration + ) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.accessExpiration = Duration.ofMillis(accessExpiration); + } + + // AccessToken 생성 + public String createAccessToken(AuthMember member) { + return createToken(member, accessExpiration); + } + + /** 토큰에서 이메일 가져오기 + * + * @param token 유저 정보를 추출할 토큰 + * @return 유저 이메일을 토큰에서 추출합니다 + */ + public String getEmail(String token) { + try { + return getClaims(token).getPayload().getSubject(); // Parsing해서 Subject 가져오기 + } catch (JwtException e) { + return null; + } + } + + /** 토큰 유효성 확인 + * + * @param token 유효한지 확인할 토큰 + * @return True, False 반환합니다 + */ + public boolean isValid(String token) { + try { + getClaims(token); + return true; + } catch (JwtException e) { + return false; + } + } + + // 토큰 생성 + private String createToken(AuthMember member, Duration expiration) { + Instant now = Instant.now(); + + // 인가 정보 + String authorities = member.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + return Jwts.builder() + .subject(member.getUsername()) // User 이메일을 Subject로 + .claim("role", authorities) + .claim("email", member.getUsername()) + .issuedAt(Date.from(now)) // 언제 발급한지 + .expiration(Date.from(now.plus(expiration))) // 언제까지 유효한지 + .signWith(secretKey) // sign할 Key + .compact(); + } + + // 토큰 정보 가져오기 + private Jws getClaims(String token) throws JwtException { + return Jwts.parser() + .verifyWith(secretKey) + .clockSkewSeconds(60) + .build() + .parseSignedClaims(token); + } +} \ No newline at end of file From 8a210a36dcbfbbeb0fc50fbf55299f5b0bf73a94 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Fri, 22 May 2026 13:40:41 +0900 Subject: [PATCH 22/42] =?UTF-8?q?feat=20:=20Token=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/filter/JwtAuthFilter.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/filter/JwtAuthFilter.java diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/filter/JwtAuthFilter.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/filter/JwtAuthFilter.java new file mode 100644 index 00000000..deaea964 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/filter/JwtAuthFilter.java @@ -0,0 +1,72 @@ +package com.example.umc10th.global.filter; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import com.example.umc10th.global.service.CustomUserDetailsService; +import com.example.umc10th.global.util.JwtUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.io.IOException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.filter.OncePerRequestFilter; + +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException, java.io.IOException { + + try { + // 토큰 가져오기 + String token = request.getHeader("Authorization"); + // token이 없거나 Bearer가 아니면 넘기기 + if (token == null || !token.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + // Bearer이면 추출 + token = token.replace("Bearer ", ""); + // AccessToken 검증하기: 올바른 토큰이면 + if (jwtUtil.isValid(token)) { + // 토큰에서 이메일 추출 + String email = jwtUtil.getEmail(token); + // 인증 객체 생성: 이메일로 찾아온 뒤, 인증 객체 생성 + UserDetails user = customUserDetailsService.loadUserByUsername(email); + Authentication auth = new UsernamePasswordAuthenticationToken( + user, + null, + user.getAuthorities() + ); + // 인증 완료 후 SecurityContextHolder에 넣기 + SecurityContextHolder.getContext().setAuthentication(auth); + } + filterChain.doFilter(request, response); + } catch (Exception e) { + ObjectMapper mapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED; + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + ApiResponse errorResponse = ApiResponse.onFailure(code,null); + + mapper.writeValue(response.getOutputStream(), errorResponse); + } + } +} \ No newline at end of file From 9d4505c1250149f0306bc7fd454c3df68a653a71 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Fri, 22 May 2026 13:41:15 +0900 Subject: [PATCH 23/42] =?UTF-8?q?feat=20:=20=ED=86=A0=ED=81=B0=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/config/SecurityConfig.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index d28d0edf..39a2a788 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -1,7 +1,11 @@ package com.example.umc10th.global.config; +import com.example.umc10th.global.filter.JwtAuthFilter; import com.example.umc10th.global.security.CustomAccessDenied; import com.example.umc10th.global.security.CustomEntryPoint; +import com.example.umc10th.global.service.CustomUserDetailsService; +import com.example.umc10th.global.util.JwtUtil; +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,10 +15,16 @@ 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; @Configuration @EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { + + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + private final String[] allowUris = { //swagger 허용 // 자유롭게 이용할 수 있는 주소 (비로그인) @@ -29,14 +39,21 @@ public class SecurityConfig { public SecurityFilterChain SecurityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) + // URI 허용 여부 .authorizeHttpRequests(requests ->requests + // public API 허용 .requestMatchers(allowUris).permitAll() + // 그 이외의 API는 인증 필요 .anyRequest().authenticated() ) + //폼 로그인 .formLogin(form -> form .defaultSuccessUrl("/swagger-ui/index.html", true) .permitAll() ) + //세션 + .sessionManagement(AbstractHttpConfigurer::disable) + .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") @@ -67,4 +84,10 @@ public CustomAccessDenied customAccessDenied() { public CustomEntryPoint customEntryPoint() { return new CustomEntryPoint(); } + + @Bean + public JwtAuthFilter jwtAuthFilter() { + return new JwtAuthFilter(jwtUtil, customUserDetailsService); + } + } From 9c4dfb837e9b414c9b5d616a08bc2f936fc0e3ed Mon Sep 17 00:00:00 2001 From: Gibeom Date: Fri, 22 May 2026 13:42:18 +0900 Subject: [PATCH 24/42] =?UTF-8?q?feat=20:=20AuthMember=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=9C=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index 941d296e..151410a5 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -26,13 +26,12 @@ public class MemberController { //마이페이지 @GetMapping("/v1/members/me") public ResponseEntity> getInfo( - @AuthenticationPrincipal Long memberId - ){ + @AuthenticationPrincipal AuthMember member + ){ BaseSuccessCode code = MemberSuccessCode.OK; - MemberResDTO.GetInfo result = memberService.getInfo(memberId); return ResponseEntity .status(code.getStatus()) - .body(ApiResponse.onSuccess(code, result)); + .body(ApiResponse.onSuccess(code, memberService.getInfo(member))); } // 홈화면 @@ -74,4 +73,17 @@ public ResponseEntity> signUp( .status(MemberSuccessCode.CREATED.getStatus()) .body(ApiResponse.onSuccess(MemberSuccessCode.CREATED, null)); } + + //로그인 + @PostMapping("/auth/login") + public ResponseEntity> login( + @RequestBody @Valid MemberReqDTO.Login req + ){ + MemberResDTO.LoginResult result = memberService.login(req); + return ResponseEntity + .status(MemberSuccessCode.OK.getStatus()) + .body(ApiResponse.onSuccess(MemberSuccessCode.OK, result)); + } + + } From 725df87bbd3cfbb09155c25830d584e291e29fe5 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Fri, 22 May 2026 13:42:53 +0900 Subject: [PATCH 25/42] =?UTF-8?q?feat=20:=20MemberService=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/service/MemberService.java | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index 0d9b76f1..1cd78c70 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -13,6 +13,8 @@ import com.example.umc10th.domain.mission.converter.MissionConverter; import com.example.umc10th.domain.mission.dto.MissionResDTO; import com.example.umc10th.domain.mission.entity.Mission; +import com.example.umc10th.global.entity.AuthMember; +import com.example.umc10th.global.util.JwtUtil; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -29,11 +31,15 @@ public class MemberService { private final MemberRepository memberRepository; private final MemberMissionRepository memberMissionRepository; private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; - public MemberResDTO.GetInfo getInfo(Long memberId) { - Member member = memberRepository.findById(memberId) + public MemberResDTO.GetInfo getInfo( + AuthMember member + ) { + Member User = member.getMember(); + memberRepository.findByEmail(User.getEmail()) .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); - return MemberConverter.toGetInfo(member); + return MemberConverter.toGetInfo(User); } //홈화면 (진행중인 미션 10개씩 페이징) @@ -68,6 +74,16 @@ public List getMissionsByStatus( return MissionConverter.toMissionDtoList(missions); } + public MemberResDTO.LoginResult login(MemberReqDTO.Login req) { + Member member = memberRepository.findByEmail(req.email()) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + if (!passwordEncoder.matches(req.password(), member.getPassword())) { + throw new MemberException(MemberErrorCode.INVALID_PASSWORD); + } + String accessToken = jwtUtil.createAccessToken(new AuthMember(member)); + return MemberConverter.toLoginResult(accessToken); + } + public void signUp(MemberReqDTO.SignUp req) { //닉네임 혹은 이메일이 이미 존재할 때 if(memberRepository.existsByEmail(req.email())){ From f9e0cd90e1a1eb3bb719bdd666eca1892c7af13d Mon Sep 17 00:00:00 2001 From: Gibeom Date: Fri, 22 May 2026 13:43:42 +0900 Subject: [PATCH 26/42] =?UTF-8?q?feat=20:=20PassWord=20Error=EC=BD=94?= =?UTF-8?q?=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/exception/code/MemberErrorCode.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index 84282c58..d29ec146 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -19,7 +19,10 @@ public enum MemberErrorCode implements BaseErrorCode { NICKNAME_DUPLICATED(HttpStatus.CONFLICT, "COMMON409_2", "이미 존재하는 닉네임입니다." - + ), + INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, + "MEMBER401_1", + "비밀번호가 올바르지 않습니다." ) ; private final HttpStatus status; From 937957f372af14ac0cf9df114d552522b416a6d3 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Fri, 22 May 2026 13:44:20 +0900 Subject: [PATCH 27/42] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/umc10th/domain/member/dto/MemberReqDTO.java | 7 +++++++ .../example/umc10th/domain/member/dto/MemberResDTO.java | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java index 047e254a..b16230df 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java @@ -21,4 +21,11 @@ public record SignUp ( Gender gender ){} + public record Login( + @Email @NotBlank + String email, + @NotBlank + String password + ){} + } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java index 9da8ee56..9c957520 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java @@ -15,6 +15,11 @@ public record GetInfo( Integer userPoint ){} + @Builder + public record LoginResult( + String accessToken + ){} + //홈 화면 @Builder public record HomeResultDto( From 539caae592008df710b3cb7db1a546aaa0779ef2 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Fri, 22 May 2026 13:44:39 +0900 Subject: [PATCH 28/42] =?UTF-8?q?feat=20:=20Converter=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/member/converter/MemberConverter.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index e1b3c6bf..9e5733a7 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -49,6 +49,12 @@ public static MemberResDTO.HomeMissionDto toHomeMissionDto(MemberMission memberM .build(); } + public static MemberResDTO.LoginResult toLoginResult(String accessToken) { + return MemberResDTO.LoginResult.builder() + .accessToken(accessToken) + .build(); + } + public static Member toMember(MemberReqDTO.SignUp req, String encodedPasssword){ return Member.builder() .name(req.name()) From 438566d91380fd4b21ecc631d3d4f93b2c712554 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 26 May 2026 13:13:15 +0900 Subject: [PATCH 29/42] =?UTF-8?q?feat=20:=20=EC=9D=98=EC=A1=B4=EC=84=B1=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 --- Gibeom/umc10th/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Gibeom/umc10th/build.gradle b/Gibeom/umc10th/build.gradle index 53339b49..b3624447 100644 --- a/Gibeom/umc10th/build.gradle +++ b/Gibeom/umc10th/build.gradle @@ -45,6 +45,9 @@ dependencies { // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1' + + // OAuth + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' } tasks.named('test') { From 55ac5afbd58965af206d8378d7cf1fe892baa033 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 26 May 2026 13:13:55 +0900 Subject: [PATCH 30/42] =?UTF-8?q?feat=20:=20JWT=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yml | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/Gibeom/umc10th/src/main/resources/application.yml b/Gibeom/umc10th/src/main/resources/application.yml index 62eed079..e8dc9d77 100644 --- a/Gibeom/umc10th/src/main/resources/application.yml +++ b/Gibeom/umc10th/src/main/resources/application.yml @@ -3,23 +3,40 @@ spring: name: "umc10th" # "umc10th" datasource: - driver-class-name: com.mysql.cj.jdbc.Driver # MySQL JDBC ???? ??? ?? + driver-class-name: com.mysql.cj.jdbc.Driver # MySQL JDBC url: ${DB_URL} # jdbc:mysql://localhost:3306/{???????} - username: ${DB_USER} # MySQL ?? ?? - password: ${DB_PW} # MySQL ???? + username: ${DB_USER} # MySQL + password: ${DB_PW} # MySQL jpa: - database: mysql # ??? ?????? ?? ?? (MySQL) + database: mysql database-platform: org.hibernate.dialect.MySQLDialect # Hibernate?? ??? MySQL ??(dialect) ?? - show-sql: true # ??? SQL ??? ??? ???? ?? ?? + show-sql: true hibernate: - ddl-auto: update # ?????? ?? ? ?????? ???? ??? ?? + ddl-auto: update properties: hibernate: - format_sql: true # ???? SQL ??? ?? ?? ??? - + format_sql: true + security: + oauth2: + client: + registration: + kakao: + client-id: ${KAKAO_REST_API_KEY} + client-secret: ${KAKAO_REST_API_SECRET} + authorization-grant-type: authorization_code + redirect-uri: "http://localhost:8080/oauth/callback/kakao" + scope: + - profile_nickname + - account_email + provider: + kakao: + authorization-uri: "https://kauth.kakao.com/oauth/authorize" + token-uri: "https://kauth.kakao.com/oauth/token" + user-info-uri: "https://kapi.kakao.com/v2/user/me" + userNameAttribute: id jwt: token: secretKey: ${JWT_SECRET_KEY} expiration: - access: 1800000 # 30분 \ No newline at end of file + access: 1800000 #30분 \ No newline at end of file From 3dc71223fdaba0a8b5516e53413780afc11ab6b8 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 26 May 2026 13:15:54 +0900 Subject: [PATCH 31/42] =?UTF-8?q?feat=20:=20JwtUtil=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/security/util/JwtUtil.java | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java new file mode 100644 index 00000000..7dcd97c6 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java @@ -0,0 +1,109 @@ +package com.example.umc10th.global.security.util; + +import com.example.umc10th.global.security.dto.SocialType; +import com.example.umc10th.global.security.entity.AuthMember; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.stream.Collectors; + +@Component +public class JwtUtil { + + private final SecretKey secretKey; + private final Duration accessExpiration; + + public JwtUtil( + @Value("${jwt.token.secretKey}") String secret, + @Value("${jwt.token.expiration.access}") Long accessExpiration + ) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.accessExpiration = Duration.ofMillis(accessExpiration); + } + + // AccessToken 생성 + public String createAccessToken(AuthMember member) { + return createToken(member, accessExpiration); + } + + /** 토큰에서 유저 식별자(이메일) 가져오기 + * + * @param token 유저 정보를 추출할 토큰 + * @return subject에 저장된 유저 식별자를 반환합니다 + */ + public String getUid(String token) { + try { + return getClaims(token).getPayload().getSubject(); + } catch (JwtException e) { + return null; + } + } + + /** 토큰에서 소셜 로그인 타입 가져오기 + * + * @param token 유저 정보를 추출할 토큰 + * @return 유저 소셜 로그인타입을 토큰에서 추출합니다 + */ + public SocialType getSocialType(String token) { + try { + return SocialType.valueOf(getClaims(token).getPayload().get("social_type").toString().toUpperCase()); // Parsing해서 Subject 가져오기 + } catch (JwtException e) { + return null; + } + } + + /** 토큰 유효성 확인 + * + * @param token 유효한지 확인할 토큰 + * @return True, False 반환합니다 + */ + public boolean isValid(String token) { + try { + getClaims(token); + return true; + } catch (JwtException e) { + return false; + } + } + + // 토큰 생성 + private String createToken(AuthMember member, Duration expiration) { + Instant now = Instant.now(); + + // 인가 정보 + String authorities = member.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + return Jwts.builder() + .subject(member.getUsername()) // User 이메일을 Subject로 + .claim("role", authorities) + .claim("social_type", member.getMember().getSocialType()) + .issuedAt(Date.from(now)) // 언제 발급한지 + .expiration(Date.from(now.plus(expiration))) // 언제까지 유효한지 + .signWith(secretKey) // sign할 Key + .compact(); + } + + // 토큰 정보 가져오기 + private Jws getClaims(String token) throws JwtException { + return Jwts.parser() + .verifyWith(secretKey) + .clockSkewSeconds(60) + .build() + .parseSignedClaims(token); + } + + +} \ No newline at end of file From 84af938a9296c66342679fc6afddc9a7190b3333 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 26 May 2026 13:16:33 +0900 Subject: [PATCH 32/42] =?UTF-8?q?feat=20:=20JWT=20=ED=95=84=ED=84=B0=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 --- .../global/security/filter/JwtAuthFilter.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java new file mode 100644 index 00000000..5ae3d288 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java @@ -0,0 +1,75 @@ +package com.example.umc10th.global.security.filter; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import com.example.umc10th.global.security.dto.SocialType; +import com.example.umc10th.global.security.service.CustomUserDetailsService; +import com.example.umc10th.global.security.util.JwtUtil; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + + try { + // 토큰 가져오기 + String token = request.getHeader("Authorization"); + // token이 없거나 Bearer가 아니면 넘기기 + if (token == null || !token.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + // Bearer이면 추출 + token = token.replace("Bearer ", ""); + // AccessToken 검증하기: 올바른 토큰이면 + if (jwtUtil.isValid(token)) { + // 토큰에서 이메일 추출 + String uid = jwtUtil.getUid(token); + SocialType socialType = jwtUtil.getSocialType(token); + // 인증 객체 생성: 이메일로 찾아온 뒤, 인증 객체 생성 + UserDetails member = customUserDetailsService.loadUserByUidAndSocialType(socialType, uid); + Authentication auth = new UsernamePasswordAuthenticationToken( + member, + null, + member.getAuthorities() + ); + // 인증 완료 후 SecurityContextHolder에 넣기 + SecurityContextHolder.getContext().setAuthentication(auth); + } + filterChain.doFilter(request, response); + } catch (Exception e) { + ObjectMapper mapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED; + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + ApiResponse errorResponse = ApiResponse.onFailure(code,null); + + mapper.writeValue(response.getOutputStream(), errorResponse); + } + } +} \ No newline at end of file From 2be1a916b982e1445ea53cbd5b52a4acb1542811 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 26 May 2026 13:17:35 +0900 Subject: [PATCH 33/42] =?UTF-8?q?feat=20:=20SecurityConfig=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 --- .../umc10th/global/config/SecurityConfig.java | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 39a2a788..50603137 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -1,17 +1,18 @@ package com.example.umc10th.global.config; -import com.example.umc10th.global.filter.JwtAuthFilter; +import com.example.umc10th.global.security.filter.JwtAuthFilter; import com.example.umc10th.global.security.CustomAccessDenied; import com.example.umc10th.global.security.CustomEntryPoint; -import com.example.umc10th.global.service.CustomUserDetailsService; -import com.example.umc10th.global.util.JwtUtil; +import com.example.umc10th.global.security.handler.OAuthSuccessHandler; +import com.example.umc10th.global.security.service.CustomOAuthService; +import com.example.umc10th.global.security.service.CustomUserDetailsService; +import com.example.umc10th.global.security.util.JwtUtil; 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.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @@ -24,6 +25,7 @@ public class SecurityConfig { private final JwtUtil jwtUtil; private final CustomUserDetailsService customUserDetailsService; + private final CustomOAuthService customOAuthService; private final String[] allowUris = { //swagger 허용 @@ -33,6 +35,7 @@ public class SecurityConfig { "/v3/api-docs/**", //로그인 "/api/auth/**", + "/oauth/**", }; @Bean @@ -43,14 +46,14 @@ public SecurityFilterChain SecurityFilterChain(HttpSecurity http) throws Excepti .authorizeHttpRequests(requests ->requests // public API 허용 .requestMatchers(allowUris).permitAll() + .requestMatchers("/oauth2/authorization/**").permitAll() // 로그인 진입 주소 + .requestMatchers("/oauth/callback/kakao").permitAll() // ⭐️ 카카오 콜백 주소 허용! // 그 이외의 API는 인증 필요 .anyRequest().authenticated() ) //폼 로그인 - .formLogin(form -> form - .defaultSuccessUrl("/swagger-ui/index.html", true) - .permitAll() - ) + .formLogin(AbstractHttpConfigurer::disable) + //세션 .sessionManagement(AbstractHttpConfigurer::disable) .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) @@ -64,6 +67,23 @@ public SecurityFilterChain SecurityFilterChain(HttpSecurity http) throws Excepti .accessDeniedHandler(customAccessDenied()) .authenticationEntryPoint(customEntryPoint()) ) + //OAuth + .oauth2Login(oauth -> oauth + // 인증 엔트리 포인트 + .authorizationEndpoint(auth -> auth + .baseUri("/oauth/authorize") + ) + // 콜백 주소 + .redirectionEndpoint(redirect -> redirect + .baseUri("/oauth/callback/**") + ) + //인증 완료 후 정보 활용 + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuthService) + ) + // 성공 시 JWT 토큰 발행할 핸들러 + .successHandler(oAuthSuccessHandler()) + ) ; return http.build(); } @@ -85,9 +105,12 @@ public CustomEntryPoint customEntryPoint() { return new CustomEntryPoint(); } - @Bean - public JwtAuthFilter jwtAuthFilter() { + private JwtAuthFilter jwtAuthFilter() { return new JwtAuthFilter(jwtUtil, customUserDetailsService); } + @Bean + public OAuthSuccessHandler oAuthSuccessHandler() { + return new OAuthSuccessHandler(jwtUtil); + } } From 947ab523beb3f795252929b8fb6303d4d176e952 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 26 May 2026 13:18:53 +0900 Subject: [PATCH 34/42] =?UTF-8?q?feat=20:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=EC=97=90=EC=84=9C=20=ED=86=A0=ED=81=B0=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EB=A6=AC=ED=84=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/member/controller/MemberController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index 151410a5..7ce6a23e 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -8,7 +8,7 @@ import com.example.umc10th.global.apiPayload.ApiResponse; import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; -import com.example.umc10th.global.entity.AuthMember; +import com.example.umc10th.global.security.entity.AuthMember; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; From 65b263a5891ec5848ba886d50c4a4e2ed57f0bb0 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 26 May 2026 13:19:32 +0900 Subject: [PATCH 35/42] =?UTF-8?q?feat=20:=20MemberService=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 --- .../example/umc10th/domain/member/service/MemberService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index 1cd78c70..bfb68f16 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -13,8 +13,8 @@ import com.example.umc10th.domain.mission.converter.MissionConverter; import com.example.umc10th.domain.mission.dto.MissionResDTO; import com.example.umc10th.domain.mission.entity.Mission; -import com.example.umc10th.global.entity.AuthMember; -import com.example.umc10th.global.util.JwtUtil; +import com.example.umc10th.global.security.entity.AuthMember; +import com.example.umc10th.global.security.util.JwtUtil; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; From f0ca08ddc28a21c570ae94d7f28a4ded45d073a8 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 26 May 2026 13:21:07 +0900 Subject: [PATCH 36/42] =?UTF-8?q?feat=20:=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20=EB=8B=B4=EC=9D=84=20DTO(OAuth,=20Kakao)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/security/dto/KakaoDTO.java | 32 +++++++++++++++++++ .../umc10th/global/security/dto/OAuthDTO.java | 10 ++++++ .../global/security/dto/SocialType.java | 5 +++ 3 files changed, 47 insertions(+) create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/KakaoDTO.java create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/OAuthDTO.java create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/SocialType.java diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/KakaoDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/KakaoDTO.java new file mode 100644 index 00000000..d1dcdbfd --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/KakaoDTO.java @@ -0,0 +1,32 @@ +package com.example.umc10th.global.security.dto; + + +import lombok.RequiredArgsConstructor; + +//카카오용 DTO +@RequiredArgsConstructor +public class KakaoDTO implements OAuthDTO { + private final String id; + private final String name; + private final String email; + + @Override + public SocialType getSocialType(){ + return SocialType.KAKAO; + } + + @Override + public String getSocialUid(){ + return id; + } + + @Override + public String getSocialEmail(){ + return email; + } + + @Override + public String getName(){ + return name; + } +} diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/OAuthDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/OAuthDTO.java new file mode 100644 index 00000000..dd9aefe5 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/OAuthDTO.java @@ -0,0 +1,10 @@ +package com.example.umc10th.global.security.dto; + + +//공통 DTO (OAuth 제공자에 따라 정보가 다를 것을 대비한 OAuthDTO) +public interface OAuthDTO { + SocialType getSocialType(); + String getSocialUid(); + String getSocialEmail(); + String getName(); +} diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/SocialType.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/SocialType.java new file mode 100644 index 00000000..7b94aaca --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/SocialType.java @@ -0,0 +1,5 @@ +package com.example.umc10th.global.security.dto; + +public enum SocialType { + KAKAO +} From 1909e627c507ac8eab62b3a5a806173405681ef1 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 26 May 2026 13:22:35 +0900 Subject: [PATCH 37/42] =?UTF-8?q?feat=20:=20Entity(=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=EC=97=90=20=EB=8B=B4=EA=B8=B8=20=EA=B0=9D=EC=B2=B4)=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 --- .../global/security/entity/AuthMember.java | 29 +++++++++++++++ .../global/security/entity/OAuthMember.java | 37 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java new file mode 100644 index 00000000..4eb2018f --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java @@ -0,0 +1,29 @@ +package com.example.umc10th.global.security.entity; + +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.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() { + return List.of(); + } + @Override + public String getPassword() { + return null; + } + @Override + public String getUsername() { + return member.getSocialUid(); + } +} diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java new file mode 100644 index 00000000..2998ef75 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java @@ -0,0 +1,37 @@ +package com.example.umc10th.global.security.entity; + + +import com.example.umc10th.domain.member.entity.Member; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +//OAuth 작업을 위한 객체 +@RequiredArgsConstructor +public class OAuthMember implements OAuth2User { + + @Getter + private final Member member; + private final Map attributes; + + @Override + public Map getAttributes(){ + return attributes; + } + + @Override + public Collection getAuthorities(){ + return List.of(); + } + + @Override + public String getName() { + return member.getSocialUid(); + } + +} From d850ad74ae128eb31525b5de83a262b52fbe6a2a Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 26 May 2026 13:23:57 +0900 Subject: [PATCH 38/42] =?UTF-8?q?feat=20:=20=ED=95=B8=EB=93=A4=EB=9F=AC=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/handler/OAuthSuccessHandler.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java new file mode 100644 index 00000000..61e01d70 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java @@ -0,0 +1,58 @@ +package com.example.umc10th.global.security.handler; + + +import com.example.umc10th.domain.member.converter.MemberConverter; +import com.example.umc10th.domain.member.dto.MemberResDTO; +import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import com.example.umc10th.global.security.entity.AuthMember; +import com.example.umc10th.global.security.entity.OAuthMember; +import com.example.umc10th.global.security.util.JwtUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import java.io.IOException; + +//사용자 정보를 가져오고 서비스가 실행된 다음 실행되는 핸들러 +@RequiredArgsConstructor +public class OAuthSuccessHandler implements AuthenticationSuccessHandler { + + private final JwtUtil jwtUtil; + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) throws IOException, ServletException { + // 사전 작업: Response 매핑할 ObjectMapper 선언 + ObjectMapper objectMapper = new ObjectMapper(); + BaseSuccessCode code = MemberSuccessCode.OK; + + // Content-Type, Status 설정 + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + // 인증 객체 컨테이너에서 OAuth 인증 객체 가져오기 + OAuthMember member = (OAuthMember) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + // 토큰 제작을 위해 OAuth 인증 객체에서 Member 추출 -> AuthMember 제작 + String accessToken = jwtUtil.createAccessToken(new AuthMember(member.getMember())); + + // 응답 통일 객체 래핑 + ApiResponse responseBody = ApiResponse.onSuccess( + code, + MemberConverter.toLogin(accessToken) + ); + + // 응답 출력 + objectMapper.writeValue(response.getOutputStream(), responseBody); + } +} \ No newline at end of file From 01e87fbbca5a1790a95aa3e16d0ed7fb84902963 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 26 May 2026 13:24:45 +0900 Subject: [PATCH 39/42] =?UTF-8?q?feat=20:=20JWT=20=EB=B9=84=EC=A6=88?= =?UTF-8?q?=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/service/CustomOAuthService.java | 71 +++++++++++++++++++ .../service/CustomUserDetailsService.java | 28 ++++++++ 2 files changed, 99 insertions(+) create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java create mode 100644 Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java new file mode 100644 index 00000000..44817c03 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java @@ -0,0 +1,71 @@ +package com.example.umc10th.global.security.service; + + +import com.example.umc10th.domain.member.converter.MemberConverter; +import com.example.umc10th.domain.member.entity.Member; +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.dto.KakaoDTO; +import com.example.umc10th.global.security.dto.OAuthDTO; +import com.example.umc10th.global.security.dto.SocialType; +import com.example.umc10th.global.security.entity.OAuthMember; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Map; + +//OAuth 과정 중 사용자 정보를 가져온 다음 실행되는 서비스 +@Service +@RequiredArgsConstructor +public class CustomOAuthService extends DefaultOAuth2UserService { + + private final MemberRepository memberRepository; + + @Override + public OAuth2User loadUser( + OAuth2UserRequest userRequest + ) throws OAuth2AuthenticationException { + // (필수) 인증 서버의 일회성 토큰을 이용해 정보 조회 & 유저 객체 생성 + OAuth2User oAuthMember = super.loadUser(userRequest); + + // 유저 객체에서 정보 추출 + SocialType providerId; + String socialUid; + try { + providerId = SocialType.valueOf(userRequest.getClientRegistration().getRegistrationId().toUpperCase()); + socialUid = String.valueOf((Long) oAuthMember.getAttribute("id")); + } catch (IllegalArgumentException e) { + throw new MemberException(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER); + } + + // OAuth 공통 정보 DTO로 매핑 + OAuthDTO dto; + switch (providerId) { + case KAKAO -> { + Map kakaoAccount = oAuthMember.getAttribute("kakao_account"); + if (kakaoAccount == null) { + throw new MemberException(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER); + } + Map profile = (Map) kakaoAccount.get("profile"); + String email = kakaoAccount.get("email") != null ? kakaoAccount.get("email").toString() : ""; + String name = (profile != null && profile.get("nickname") != null) ? profile.get("nickname").toString() : ""; + dto = new KakaoDTO(socialUid, name, email); + } + default -> throw new MemberException(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER); + } + + // DB 저장: 있다면 그 데이터 가져오고 없으면 새로 저장 + Member member = memberRepository.findBySocialTypeAndSocialUid(providerId, socialUid) + .orElseGet(() -> { + Member newMember = MemberConverter.toMember(dto); + memberRepository.save(newMember); + return newMember; + }); + return new OAuthMember(member, oAuthMember.getAttributes()); + } +} diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java new file mode 100644 index 00000000..e8c3e7b8 --- /dev/null +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java @@ -0,0 +1,28 @@ +package com.example.umc10th.global.security.service; + +import com.example.umc10th.domain.member.entity.Member; +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.dto.SocialType; +import com.example.umc10th.global.security.entity.AuthMember; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService{ + + private final MemberRepository memberRepository; + + public UserDetails loadUserByUidAndSocialType( + SocialType socialType, + String username + ) throws UsernameNotFoundException{ + Member member = memberRepository.findBySocialTypeAndSocialUid(socialType, username) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + return new AuthMember(member); + } +} From 00e45b62f0b3be47bd3ac056e43a350269a36950 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 26 May 2026 13:28:21 +0900 Subject: [PATCH 40/42] =?UTF-8?q?feat=20:=20=EB=A9=A4=EB=B2=84=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/converter/MemberConverter.java | 18 ++++++++++++++++++ .../domain/member/dto/MemberResDTO.java | 5 +++++ .../umc10th/domain/member/entity/Member.java | 12 ++++++++++-- .../member/exception/code/MemberErrorCode.java | 7 ++++++- .../member/repository/MemberRepository.java | 2 ++ 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index 9e5733a7..baf0a23e 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -4,6 +4,7 @@ import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.entity.Member; import com.example.umc10th.domain.member.entity.mapping.MemberMission; +import com.example.umc10th.global.security.dto.OAuthDTO; import org.springframework.data.domain.Page; import org.springframework.security.core.Authentication; @@ -66,4 +67,21 @@ public static Member toMember(MemberReqDTO.SignUp req, String encodedPasssword){ .nickname(req.nickname()) .build(); } + + public static Member toMember(OAuthDTO dto) { + return Member.builder() + .name(dto.getName()) + .nickname(dto.getName()) + .email(dto.getSocialEmail()) + .socialType(dto.getSocialType()) + .socialUid(dto.getSocialUid()) + .userPoint(0) + .build(); + } + + public static MemberResDTO.Login toLogin(String accessToken) { + return MemberResDTO.Login.builder() + .accessToken(accessToken) + .build(); + } } diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java index 9c957520..4b672d47 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java @@ -20,6 +20,11 @@ public record LoginResult( String accessToken ){} + @Builder + public record Login( + String accessToken + ){} + //홈 화면 @Builder public record HomeResultDto( diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java index 51df7d92..fb197716 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -4,6 +4,7 @@ import com.example.umc10th.domain.member.enums.Gender; import com.example.umc10th.domain.store.entity.Region; import com.example.umc10th.global.entity.BaseEntity; +import com.example.umc10th.global.security.dto.SocialType; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; @@ -37,7 +38,7 @@ public class Member extends BaseEntity{ @Column(name = "nickname", nullable = false) private String nickname; - @Column(name = "phoneNumber", nullable = false) + @Column(name = "phoneNumber") private String phoneNumber; @Column(name = "user_point") @@ -46,9 +47,16 @@ public class Member extends BaseEntity{ @Column(name = "email") private String email; - @Column(name = "password", nullable = false) + @Column(name = "password") private String password; + @Column(name = "social_type") + @Enumerated(EnumType.STRING) + private SocialType socialType; + + @Column(name = "social_uid") + private String socialUid; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "region_id") private Region region; diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index d29ec146..844b6740 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -23,7 +23,12 @@ public enum MemberErrorCode implements BaseErrorCode { INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "MEMBER401_1", "비밀번호가 올바르지 않습니다." - ) + ), + NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.BAD_REQUEST, + "404_3", + "제공하지 않는 인증자입니다." + + ), ; private final HttpStatus status; private final String code; diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index 9813a157..70a4b847 100644 --- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -1,6 +1,7 @@ package com.example.umc10th.domain.member.repository; import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.global.security.dto.SocialType; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; @@ -9,4 +10,5 @@ public interface MemberRepository extends JpaRepository { Boolean existsByEmail(String email); Optional findByEmail(String email); Boolean existsByNickname(String nickname); + Optional findBySocialTypeAndSocialUid(SocialType socialType, String socialUid); } From 1b7ccc7b8daeff2efbea6f480a8d66b194d56184 Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 26 May 2026 13:29:21 +0900 Subject: [PATCH 41/42] =?UTF-8?q?docs=20:=209=EC=A3=BC=EC=B0=A8=20?= =?UTF-8?q?=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=9A=94=EC=95=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gibeom/umc10th/keyword_summary/ch09.md | 53 ++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 Gibeom/umc10th/keyword_summary/ch09.md diff --git a/Gibeom/umc10th/keyword_summary/ch09.md b/Gibeom/umc10th/keyword_summary/ch09.md new file mode 100644 index 00000000..df76c882 --- /dev/null +++ b/Gibeom/umc10th/keyword_summary/ch09.md @@ -0,0 +1,53 @@ +- 세션과 토큰의 차이는? + + 세션 방식은 사용자의 정보를 서버가 저장함 + + - 이용자 수가 많으면 서버 부하가 커짐(모든 사용자 정보를 테이블로 보유하고 있음) + + 토큰 방식은 사용자가 내 정보가 포함된 토큰을 내밀기만 하면 됨 + + - 서버의 부담이 적음. + - 다만 사용자의 정보를 저장하지 않기 때문에 할 수 없는 기능도 있음. + + 세션 : 입장권인데 적혀있는 게 거의 없음. 사이트가 입장권 검사하고 저장해놓음. + + 토큰 : 뭐가 많이 적혀있는 입장권. 사이트는 입장권 들고있으면 입장 허용해줌. + + (참고자료) https://youtu.be/XXseiON9CV0?si=Am8JlSoLV0SiZX0W + +- 엑세스 토큰과 리프레시 토큰이란? + + 액세스 토큰 : 접근하기 위해 필요한 기본적인 토큰, 해킹 공격 방지를 위해 유효기간이 짧다. + + 리프레쉬 토큰 : 만료된 액세스 토큰을 재생성해주는 토큰. + +- OAuth 1.0과 OAuth 2.0의 차이는? + + OAuth 2.0 은 1.0과 달리 복잡한 암호화 서명 과정을 간소화하고, 웹/모바일 등 다양한 환경에 맞춘 인증 방식을 도입해 속도와 확장성을 대폭 개선한 버전 + + 핵심 차이점 + + | 구분 | OAuth 1.0 | OAuth 2.0 | + | --- | --- | --- | + | 핵심 매커니즘 | 모든 요청에 디지털 서명 필요 | 발급받은 액세스 토큰만으로 인증 | + | 암호화 요구사항 | HTTPS가 필수가 아님 | HTTPS(TLS) 필수 | + | 역할의 분리 | 단순함(Client, Server, User) | 인증서버와 리소스 서버의 분리 | + | 토큰 만료 | 토큰 만료 개념이 없거나 복잡 | 만료기간이 존재하며, 리프레시 토큰으로 갱신가능 | + | 지원 환경 | 웹 브라우저 기반 애플리케이션 중심 | 모바일 앱, Iot, 데스크톱 등 다양한 환경 지원 | + + + 왜 2.0으로 바뀌었을까? + + 1. 개발의 복잡성 해결(디지털 서명의 폐지) + + 1.0은 API를 호출할 때마다 복잡한 암호화 알고리즘으로 디지털 서명을 생성하고 이를 헤더에 담아 보내야 했음. 이 서명을 만드는 과정이 조금만 틀려도 인증이 실패했기 때문에 매우 불편했음. + + 2.0은 이 서명 과정을 과감히 버렸음. 대신 HTTPS통신을 필수로 규정해 연결 자체를 암호화하고, 발급받은 문자열 토큰만 헤더에 얹어서 보내면 되도록 단순화 했음 + + 2. 모바일 및 다양한 디바이스 지원 + + OAuth 1.0이 나올 당시에는 웹브라우저 중심의 환경이었음. 하지만 모바일 앱, IoT 등의 기기들이 생기면서 브라우저가 없거나 화면이 없는 환경에서도 인증을 처리해야 할 필요가 생김. OAuth 2.0은 시나리오별 인증방식을 여러개 제공해 맞춤형 인증을 제공함 + + 3. 대규모 서비스 분할(확장성) + + OAuth 2.0은 인증을 담당하는 서버와 실제 데이터를 가지고 있는 서버의 역할을 명확히 나눴음. 덕분에 대기업이나 대규모 서비스에서는 인증서버만 따로 구축해 트래픽을 분산할 수 있음. \ No newline at end of file From 088b79ed8e05e92f39b61569e9c87783c8555e7c Mon Sep 17 00:00:00 2001 From: Gibeom Date: Tue, 26 May 2026 13:36:15 +0900 Subject: [PATCH 42/42] =?UTF-8?q?chore:=20gitignore=EC=97=90=20.idea/=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 --- .gitignore | 1 + .idea/workspace.xml | 73 --------------------------------------------- 2 files changed, 1 insertion(+), 73 deletions(-) create mode 100644 .gitignore delete mode 100644 .idea/workspace.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9f11b755 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 8713e6d2..00000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - 1775064304989 - - - - - - \ No newline at end of file