묻고 답해요
164만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결나도코딩의 자바 기본편 - 풀코스 (20시간)
객체 생성 부분에서 헷갈려요
안녕하세요 Camera 클래스는 추상 클래스라서 객체를 생성할 수 없는 상황에서, 아래의 코드는 FactoryCam과 SpeedCam의 객체가 되는 건가요? Camera factoryCam = new FactoryCam(); factoryCam.showMainFeature(); Camera speedCam = new SpeedCam(); speedCam.showMainFeature();
-
해결됨[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지
req.session 질문
16:19의 3번에서 세션 객체를 찾아서 req.session으로 만든다고 했었는데 이전에 처음에 로그인 했을 때 req.session으로 등록된 세션 객체(13:40의 6번)는 사라진 건가요?
-
미해결스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술
MemberServiceTest의 join()에 대해서 질문 있습니다.
학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요.1. 강의 내용과 관련된 질문을 남겨주세요.2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요.(자주 하는 질문 링크: https://bit.ly/3fX6ygx)3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요.(질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG)질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요.=========================================[질문 템플릿]1. 강의 내용과 관련된 질문인가요? (예)2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예)3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예)[질문 내용]여기에 질문 내용을 남겨주세요. MemberServiceTest의 join() 부분을 시청하던 도중 의문점이 생겨 질문드립니다. 현재 저희는 MemberService의 클래스가 잘 만들어졌는지 테스트를 하는 과정에서 join, findMembers, findOne을 테스트합니다. 이런 과정에서 join을 테스트할 때, 아직 검증하지 않은 findOne을 통해서 join을 테스트하는 것이 옳은 방법인지 궁금합니다.기존에 아직 검증되지 않은 메서드를 사용해도 무방한 것일까요?? 만일 findOne이 잘 정의되어있지 않다면 아무리 잘 정의된 join도 Test에서 오류를 일으킬 것 같아서 질문드립니다.
-
미해결파이썬 무료 강의 (기본편) - 6시간 뒤면 나도 개발자
저는 이렇게 풀었습니다.
import pickle for i in range(1, 51): breef_files = open(str(i) + "주차.pickle", "wb") breef = "- "+ str(i) +"주차 주간 보고 -\n부서 : \n이름 : \n업무 요약" # print(breef) pickle.dump(breef, breef_files) breef_files.close() for i in range(1, 51): breef_files = open(str(i) + "주차.pickle", "rb") breef = pickle.load(breef_files) print(breef) breef_files.close()?
-
미해결[코드팩토리] [초급] Flutter 3.0 앱 개발 - 10개의 프로젝트로 오늘 초보 탈출!
웹뷰에 링크를 못 불러옵니다
앱 실행은 되는데 웹 링크를 못 엽니다ㅠㅠ확인 부탁드립니다 !//오류 내용I/X509Util( 5126): Failed to validate the certificate chain, error: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. E/chromium( 5126): [ERROR:ssl_client_socket_impl.cc(946)] handshake failed; returned -1, SSL error code 1, net_error -202//작성한 코드import 'package:flutter/material.dart'; import 'package:hello_world/screen/home_screen.dart'; void main() { // Flutter 프레임워크가 // 앱을 실행할 준비가 될 때까지 기다린다. WidgetsFlutterBinding.ensureInitialized(); runApp( MaterialApp( home: HomeScreen(), ), ); } import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; final homeUrl = Uri.parse('https://blog.codefactory.ai'); // ignore: must_be_immutable class HomeScreen extends StatelessWidget { WebViewController controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..loadRequest(homeUrl); HomeScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.orange, title: const Text('yoonjoo'), actions: [ IconButton( onPressed: () { controller.loadRequest(homeUrl); }, // ignore: prefer_const_constructors icon: Icon( Icons.home, ), ), ], ), body: WebViewWidget( controller: controller, ), ); } }
-
미해결[리뉴얼] 타입스크립트 올인원 : Part1. 기본 문법편
추상클래스를 상속받은 구현클래스에서 private 접근 불가관련 해서 질문 있습니다.
추상 클래스에서 private으로 선언한 것은 추상클래스를 상속받아 구현하는 실제 클래스에서도 접근이 안되고 protected로 바꾸라고 하던데어짜피 구현은 추상클래스를 상속받은 구현클래스에서 해야하는데 접근 할 수 없다면 추상클래스에서 private가 어떤 역할을 하는건지 모르겠습니다.// 추상 클래스 abstract class AbstractClass { private readonly a: string = "init"; protected b: number = 1; c: string = "기본값이 public"; abstract method(a: string): void; method2() { console.log(this.a); console.log(this.b); console.log(this.c); } } class realClass extends AbstractClass { method(a: string) { console.log(this.a); //error console.log(this.b); console.log(this.c); } } type error msg : TS2341: Property 'a' is private and only accessible within class 'AbstractClass'.
-
미해결Vue.js 시작하기 - Age of Vue.js
상위컴포넌트,하위컴포넌트간의 데이터이동에 관한 질문 드립니다.
학습한 것 중 loading..부분에 궁금한게있습니다.button을 component를 만들어서 body에 넣어준 후 button을 통해서 data loading의 값을 false로 변경할 수 있게 컨트롤 해보고 싶은데해당 경우에도 v-bind와 v-on이용하여 상위컴포넌트로 올려준 후 다시 하위컴포넌트로 보내주어야하나요?
-
미해결따라하며 배우는 노드, 리액트 시리즈 - 기본 강의
Mongo DB 연결 오류
다음과 같은 에러가 뜨는데 어떻게 해결할 수 있을까요..??
-
미해결파이썬/장고 웹서비스 개발 완벽 가이드 with 리액트
azure web app에서 배포가 계속 안됩니다...
azure web app을 통해 배포를 하던중 계속해서 오류가 발생하였습니다.처음에는/usr/local/bin/gunicorn: exec format error에러가 발생하여 제가 m1 맥북을 사용하다보면, 맥북에서 빌드한 이미지가, 서로 다른 CPU 아키텍처로 인해 서버 에러가 발생할 수 있다는 것을 알게되어 buildx을 이용하여위와 같이 설정을 하였으나 여전히 에러가 발생합니다.local에서는 아무런 문제없이 작동하나 azure에서만 문제가 발생하여 데이터베이스연결과 storage연결까지는 아무런 문제가 없었습니다.로컬에서 실행할 때는 아래와 같이 실행하였습니다.docker run --rm --publish 9999:80 \ -e DJANGO_SETTINGS_MODULE=backend.settings.prod \ -e AZURE_ACCOUNT_NAME=capstonboom \ -e AZURE_ACCOUNT_KEY="필요하시면 따로 알려드리겠습니다..." \ -e ALLOWED_HOSTS=localhost \ -e DB_HOST=capstonproject.postgres.database.azure.com \ -e DB_USER=gunhong@capstonproject \ -e DB_PASSWORD="비밀번호" \ -e DB_NAME=postgres \ keonhong/capstonproject:0.1도커파일은 아래와 같이 설정했습니다.FROM ubuntu:20.04 RUN apt-get update && apt-get install -y python3-pip && apt-get clean WORKDIR /djangoproject ADD . /djangoproject RUN pip3 install -r requirements.txt RUN which gunicorn || echo gunicorn not found EXPOSE 8000 CMD ["gunicorn", "backend.wsgi:application", "--bind", "0.0.0.0:80"]우분투 버전과 guncorn버전이 호완이 안될까 생각해서gunicorn==20.0.4 을 이용하였습니다.위와 같이 한 후 도커 허브의 레지스토리와 azure app web을 모두 삭제하고 새롭게 시작했더니/usr/local/bin/gunicorn: exec format error이 에러는 발생하지 않으나 여전히Logging is not enabled for this container.에러메세지는 출력됩니다... gunicorn 문제로 인해Logging is not enabled for this container. 도 함께 출력되었는지 생각했으나 또 다른 문제였나 봅니다...
-
해결됨[리뉴얼] 타입스크립트 올인원 : Part1. 기본 문법편
interface를 사용할 때 private 사용방법은?
impolements러 interface를 받아 class를 만들때 private이 안되는 오류를 보여주셨는데요interface를 사용하지 않고 그냥 class안에서 type을 지정하는 방법, abstract class를 이용하는 방법 모두 이해됬습니다. 하지만 interface를 사용하면 private, protected 사용이 불가한 것인지 잘 모르겠어서 질문을 올립니다. 구글링해본결과 class내에 속성으로 만들고 getter, setter를 이용하는 것으로 우회하는 방법을 사용하더라구요.(https://stackoverflow.com/questions/37791947/how-to-define-a-private-property-when-implementing-an-interface-in-typescript) 이렇게 했을 때 private의 기능인 class 밖에서는 호출 할 수 없다고 위반되는 결과가 나옵니다. 어떤식으로 해결 해야 할까요? interface Interface { readonly a: string; b: number; } class TSClass implements Interface { private readonly _a: string = "init"; get a() { return this._a; } protected _b: number = 1; get b() { return this._b; } set b(v: number) { this._b = v; } c: string = "기본값이 public"; method() { console.log(this._a); console.log(this._b); console.log(this.c); } } class inheritClass extends TSClass { method() { console.log(this._a); // error console.log(this.a); // 가능.. console.log(this._b); console.log(this.b); console.log(this.c); } } new inheritClass()._a; // error new inheritClass().a; // 가능... new inheritClass()._b; // error new inheritClass().b; // 가능.. new inheritClass().c;
-
미해결자바스크립트 알고리즘 문제풀이 입문(코딩테스트 대비)
이중 for문을 돌아야 하는 정확한 이유가 있을까요?
function solution(arr){ let copy = arr.slice().sort((a, b) => b - a); return copy.map(it => arr.indexOf(it) + 1) } let arr=[87, 89, 92, 100, 76]; console.log(solution(arr)); 제가 코테공부는 거의 안해서 잘 모르는데 시간복잡도가 연관이 있을것 같습니다 2중 for문을 돌아야 하는 이유가 있을까요?
-
미해결10주완성 C++ 코딩테스트 | 알고리즘 코딩테스트
2-I 참조 에러
안녕하세요 항상 강의 잘 보고있습니다.!궁금한 점이 있는데, go() 함수 부분에서while(true){if(ret.size() && ret.front() == '0')}참조 에러에 안걸리게 ret.size()를 꼭 적어야 한다고 말씀하셨는데, 제가 생각할땐 ret.front() == '0' 자체가 size가 1 이상이라는 것도 포함되니까 ret.size()를 적어야하는 이유를 잘 모르겠습니다.
-
해결됨[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스
state 에 대하여 질문있습니다.
const onChangeEmail = (event) => { console.log(event.target.value); setEmail(event.target.value); console.log("!@# ", email); if (email === "") { alert(); }위의 코드를 실행할경우가장 첫 입력시 아래와 같은 문제가 발생합니다.(이메일 입력란에 'ㅁ' 입력)첫번째 console.log envent.target.value는 정상적으로 출력되며두번째 console.log는 "!@#" 을 제외한 email이 출력되지 않습니다. "!@#"만 출력됨if문의 경우 공백으로 인식해서 true 동작하여 출력이 되고 있습니다.원인은 무엇인가요?추가로 event 의 문제인가 싶어onChange가 아닌 onKeyUp 을 사용 할 경우에는f12 개발 디버깅쪽 console 에 error 표시가 잔뜩 발생합니다.이유가 무엇인가요?==================================email input tag에 최초 입력 시 아래의 코드처럼 onChange 함수안에 지역변수 testb 에 할당하는 event.target.value 는 출력되지만state 에 할당된 email 은 한반작 늦은? 출력이 됩니다.ex) input box 'ㅁ' 입력 시 결과state email = '' 공백출력지역변수 testb = 'ㅁ' 출력const onChangeEmail = (event) => { let testb = event.target.value; alert(testb); console.log(event.target.value); setEmail(event.target.value); console.log("!@# ", email); 설명 부탁드립니다.
-
해결됨[2026년 출제기준] 웹디자인개발기능사 실기시험 완벽 가이드
텝메뉴 색상이 변경이 이상합니당 ㅠㅠ
처음 화면에는 이렇게 잘 뜨는데,갤러리를 누르면 두개 다 흰색으로변경되고 이후에 쭉두 메뉴 모두 흰색텝으로만 뜨는데 왜이러는걸가용?ㅠㅠ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <div class="container"> <header> <div class="header-logo"> <a href="#none"><img src="images/header-logo.png" alt="header-logo"></a> </div> <div class="navi"> <ul class="menu"> <li> <a href="#none">탑</a> <div class="sub-menu"> <a href="#none">블라우스</a> <a href="#none">티</a> <a href="#none">셔츠</a> <a href="#none">니트</a> </div> </li> <li> <a href="#none">아우터</a> <div class="sub-menu"> <a href="#none">자켓</a> <a href="#none">코트</a> <a href="#none">가디건</a> <a href="#none">머플러</a> </div> </li> <li> <a href="#none">팬츠</a> <div class="sub-menu"> <a href="#none">청바지</a> <a href="#none">짧은바지</a> <a href="#none">긴바지</a> <a href="#none">레깅스</a> </div> </li> <li> <a href="#none">악세서리</a> <div class="sub-menu"> <a href="#none">귀고리</a> <a href="#none">목걸이</a> <a href="#none">반지</a> <a href="#none">팔찌</a> </div> </li> </ul> </div> </header> <div class="slide"> <div> <a href="#none"><img src="images/slide-01.jpg" alt="slide-01"> </a> <a href="#none"><img src="images/slide-02.jpg" alt="slide-02"></a> <a href="#none"><img src="images/slide-03.jpg" alt="slide-03"></a> </div> </div> <div class="items"> <div class="news"> <div class="tab-inner"> <div class="btn"> <a class="active" href="#none">공지사항</a> <a href="#none">갤러리</a> </div> <div class="tab"> <div class="tab1"> <a href="#none">SMS 발송 모바일 서비스 개선작업 <b>2020.01.09</b></a> <a href="#none">휴대폰 인증 서비스 개선 작업 <b>2020.01.07</b></a> <a href="#none">카드사 부분 무이자 할부 이벤트 <b>2019.12.31</b></a> <a href="#none">올앳 시스템 작업 안내 <b>2019.12.20</b></a> <a href="#none">휴대폰 결제 시스템 작업 <b>2019.12.20</b> </a> </div> <div class="tab2"> <a href="#none"><img src="images/gallery-01.jpg" alt="gallery-01"></a> <a href="#none"><img src="images/gallery-02.jpg" alt="gallery-02"></a> <a href="#none"><img src="images/gallery-03.jpg" alt="gallery-03"></a> </div> </div> </div> </div> <div class="banner"> <a href="#none"><img src="images/banner.jpg" alt="banner"></a> </div> <div class="shortcut"> <a href="#none"><img src="images/shortcut.jpg" alt="shortcut"></a> </div> </div> <footer> <div class="footer-logo"> <a href="#none"><img src="images/footer-logo.png" alt="footer-logo"> </a> </div> <div class="copy"> 상호 : 엣지컴퍼니 | 대표자 : 홍길동 | 개인정보관리책임자 : 장길산 차장 <br> 사업장주소 : 서울특별시 강남구 테헤란로 123-56 </div> <div class="sns"> <a href="#none"><img src="images/sns-01.png" alt="facebook"></a> <a href="#none"><img src="images/sns-02.png" alt="twitter"></a> <a href="#none"><img src="images/sns-03.png" alt="instagram"></a> </div> </footer> </div> <script src="script/jquery-1.12.4.js"></script> <script src="script/custom.js"></script> </body> </html> @charset "utf-8"; body { background-color: #fff; color: #333333; margin: 0; font-size: 15px; } a { color: #333333; text-decoration: none; } /* 기본구도잡기 */ .container { /* border: solid 1px red; */ width: 1200px; margin: auto; } header { height: 100px; } header > div { /* border: 1px solid blue; */ height: 100px; } .header-logo { width: 200px; float: left; } .navi { width: 600px; float: right; } .slide {} .slide > div { /* border: 1px solid green; */ height: 300px; } .items { overflow: hidden; } .items > div { /* border: 1px solid pink; */ height: 200px; float: left; box-sizing: border-box; } .news { width: 500px; } .banner { width: 350px; } .shortcut { width: 350px; } footer { overflow: hidden; } footer > div { /* border: 1px solid purple; */ height: 100px; float: left; box-sizing: border-box; } .footer-logo { width: 200px; } .copy { width: 800px; } .sns { width: 200px; } /* 이미지와 텍스트 */ header { position: relative; z-index: 10; } .header-logo { line-height: 130px; } .slide { margin-bottom: 20px; } .footer-logo { line-height: 130px; } .copy { text-align: center; padding-top: 30px; } .sns { padding-top: 20px; text-align: center; } /* 슬라이드 */ .slide { position: relative; width: 1200px; height: 300px; overflow: hidden; } .slide > div { position: absolute; top: 0; left: 0; animation: slide 10s linear infinite; font-size: 0; } @keyframes slide { 0% { top: 0; } 30% { top: 0; } 35% { top: -300px; } 65% { top: -300px; } 70% { top: -600px; } 95% { top: -600px; } 100% { top: 0; } } /* 네비게이션 */ .menu { list-style: none; padding: 0; padding-top: 20px; /* border: 1px solid red; */ } .menu li { /* border: 1px solid #000; */ float: left; width: 25%; box-sizing: border-box; text-align: center; } .menu li > a { border: 1px solid black; display: block; padding: 5px; transition: 0.5s; } .menu li:hover > a { background-color: #000; color: #fff; } .sub-menu { background-color: #fff; border: 1px solid black; display: none; } .sub-menu a { display: block; padding: 5px; transition: 0.5s; } .sub-menu a:hover { background-color: #000; color: #fff; } /* 뉴스와 갤러리 텝메뉴 */ .tab-inner { width: 95%; margin: auto; } .btn {} .btn a { border: 1px solid #000; display: inline-block; width: 100px; text-align: center; padding: 5px; border-radius: 5px 5px 0 0; margin-right: -6px; border-bottom: none; margin-bottom: -1px; background-color: #ddd; } .btn a.active { background-color: #fff; } .tab {} .tab1 { border: 1px solid #000; padding: 0 10px; /* display: none; */ } .tab1 a { display: block; padding: 5px; border-bottom: 1px solid #000; } .tab1 a:last-child { border-bottom: none; } .tab1 a b { float: right; font-weight: normal; } .tab2 { border: 1px solid #000; height: 169px; text-align: center; padding-top: 20px; box-sizing: border-box; display: none; } .tab2 img { width: 130px; } // 네비게이션 $('.menu li').mouseenter(function(){ $('.sub-menu').stop().slideDown() }) $('.menu li').mouseleave(function(){ $('.sub-menu').stop().slideUp() }) // $('.menu li').mouseenter(function(){ // $(this).children('.sub-menu').stop().slideDown() // }) // $('.menu li').mouseleave(function(){ // $(this).children('.sub-menu').stop().slideUp() // }) // 텝메뉴 $('.btn a:first-child').click(function(){ $('.tab1').show() $('.tab2').hide() $(this).addClass('active') $(this).sliblings().removeClass('active') }) $('.btn a:last-child').click(function(){ $('.tab2').show() $('.tab1').hide() $(this).addClass('active') $(this).sliblings().removeClass('active') })
-
해결됨[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스
함수 선언시 질문있습니다.
이 질문은 학습용임으로 사용목적에 대해서 답변을 구하는것은 아닙니다.function onChangePasword(e){}위와 같이 사용도 가능하지만const onChangeEmail = function (event) {}와 같이 익명함수로도 선언이 가능하자나요?질문코자 하는 내용은 아래와 같이 default 로 생성시 error 가 발생합니다.onChangeEmail = function (event) {}또한 화살표 함수를 사용시에도 동일한 문제가 발생합니다.const onChangeEmail = (event) => {} 정상onChangeEmail = (event) => {} error 이유가 뭔가요?
-
미해결스프링부트 시큐리티 & JWT 강의
소셜 로그인 후 JWT 발급
SecurityConfigpackage com.example.project1.config.security; import com.example.project1.config.jwt.JwtAccessDeniedHandler; import com.example.project1.config.jwt.JwtAuthenticationEntryPoint; import com.example.project1.config.jwt.JwtProvider; import com.example.project1.config.oauth2.PrincipalOauth2UserService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.DelegatingPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import java.util.HashMap; import java.util.Map; @Configuration @RequiredArgsConstructor @EnableWebSecurity // @EnableGlobalMethodSecurity 어노테이션은 Spring Security에서 메서드 수준의 보안 설정을 활성화하는데 사용되는 어노테이션입니다. //@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) public class SecurityConfig { private final JwtProvider jwtProvider; private final PrincipalOauth2UserService principalOauth2UserService; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 스프링 시큐리티에서 제공하는 로그인 페이지를 안쓰기 위해 .httpBasic().disable() // JWt 방식을 제대로 쓰려고 하면 프론트엔드가 분리된 환경을 가정하고 해야합니다. .csrf().disable() .formLogin().disable() .logout().disable() // JWT 방식은 세션저장을 사용하지 않기 때문에 꺼줍니다. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http .authorizeRequests() .antMatchers("/api/v1/boards/write") .access("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')") .antMatchers("/api/v1/boards/modify") .access("hasRole('ROLE_USER')") .antMatchers("/api/v1/boards/remove") .access("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')") .antMatchers("/api/v1/admin/**") .access("hasRole('ROLE_ADMIN')") // /success-oauth 엔드포인트에 대해 인증된 사용자만 접근 가능하도록 설정 // .antMatchers("/success-oauth").authenticated() .antMatchers("/swagger-resources/**").permitAll() .antMatchers("/swagger-ui/**").permitAll() .antMatchers("/api/v1/users/**").permitAll(); http // JWT Token을 위한 Filter를 아래에서 만들어 줄건데, // 이 Filter를 어느위치에서 사용하겠다고 등록을 해주어야 Filter가 작동이 됩니다. // security 로직에 JwtFilter 등록 // .addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class) .apply(new JwtSecurityConfig(jwtProvider)); // 에러 방지 http .exceptionHandling() .authenticationEntryPoint(new JwtAuthenticationEntryPoint()) .accessDeniedHandler(new JwtAccessDeniedHandler()); // oauth2 http // oauth2Login() 메서드는 OAuth 2.0 프로토콜을 사용하여 소셜 로그인을 처리하는 기능을 제공합니다. .oauth2Login() // .defaultSuccessUrl("/success-oauth") // OAuth2 로그인 성공 이후 사용자 정보를 가져올 때 설정 담당 .userInfoEndpoint() // OAuth2 로그인 성공 시, 후작업을 진행할 서비스 .userService(principalOauth2UserService) .and() .defaultSuccessUrl("/success-oauth"); return http.build(); } @Bean PasswordEncoder passwordEncoder() { String idForEncode = "bcrypt"; Map<String, PasswordEncoder> encoders = new HashMap<>(); encoders.put(idForEncode, new BCryptPasswordEncoder()); return new DelegatingPasswordEncoder(idForEncode, encoders); } }JwtProviderpackage com.example.project1.config.jwt; import com.example.project1.domain.jwt.TokenDTO; import io.jsonwebtoken.*; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import io.jsonwebtoken.security.Keys; import javax.xml.bind.DatatypeConverter; import java.security.Key; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.stream.Collectors; @Slf4j @Component public class JwtProvider { private static final String AUTHORITIES_KEY = "auth"; @Value("${jwt.access.expiration}") private long accessTokenTime; @Value("${jwt.refresh.expiration}") private long refreshTokenTime; private Key key; public JwtProvider( @Value("${jwt.secret_key}") String secret_key) { byte[] secretByteKey = DatatypeConverter.parseBase64Binary(secret_key); this.key = Keys.hmacShaKeyFor(secretByteKey); } // 유저 정보를 가지고 AccessToken, RefreshToken 을 생성하는 메소드 public TokenDTO createToken(Authentication authentication) { // 권한 가져오기 // authentication 객체에서 권한 정보(GrantedAuthority)를 가져와 문자열 형태로 변환한 후, // 쉼표로 구분하여 조인한 결과를 authorities 변수에 저장합니다. 따라서 authorities는 권한 정보를 문자열 형태로 가지게 됩니다. // 권한 정보를 문자열로 변환하여 클레임에 추가하는 방식 String authorities = authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); long now = (new Date()).getTime(); Date now2 = new Date(); // AccessToken 생성 Date accessTokenExpire = new Date(now + this.accessTokenTime); String accessToken = Jwts.builder() // 내용 sub : 유저의 이메일 // 토큰 제목 .setSubject(authentication.getName()) .setIssuedAt(now2) // 클레임 id : 유저 ID .claim(AUTHORITIES_KEY, authorities) // 내용 exp : 토큰 만료 시간, 시간은 NumericDate 형식(예: 1480849143370)으로 하며 // 항상 현재 시간 이후로 설정합니다. .setExpiration(accessTokenExpire) // 서명 : 비밀값과 함께 해시값을 ES256 방식으로 암호화 .signWith(key, SignatureAlgorithm.HS256) .compact(); log.info("accessToken : " + accessToken); // RefreshToken 생성 Date refreshTokenExpire = new Date(now + this.refreshTokenTime); String refreshToken = Jwts.builder() .setSubject(authentication.getName()) .claim(AUTHORITIES_KEY, authorities) .setIssuedAt(now2) .setExpiration(refreshTokenExpire) .signWith(key, SignatureAlgorithm.HS256) .compact(); log.info("refreshToken : " + refreshToken); return TokenDTO.builder() .grantType("Bearer") .accessToken(accessToken) .refreshToken(refreshToken) .accessTokenTime(accessTokenExpire) .refreshTokenTime(refreshTokenExpire) // principalDeatails에서 getUserName 메소드가 반환한 것을 담아준다. // 이메일을 반환하도록 구성했으니 이메일이 반환됩니다. .userEmail(authentication.getName()) .build(); } // 소셜 로그인 성공시 JWT 발급 public TokenDTO createToken2(UserDetails userDetails) { long now = (new Date()).getTime(); Date now2 = new Date(); // userDetails.getAuthorities()는 사용자의 권한(authorities) 정보를 가져오는 메서드입니다. // claims.put("roles", userDetails.getAuthorities()) 코드는 사용자의 권한 정보를 클레임에 추가하는 것입니다. // 클레임에는 "roles"라는 키로 사용자의 권한 정보가 저장되며, 해당 권한 정보는 JWT의 페이로드 부분에 포함됩니다. Claims claims = Jwts.claims().setSubject(userDetails.getUsername()); claims.put("roles", userDetails.getAuthorities()); // access token Date accessTokenExpire = new Date(now + this.accessTokenTime); String accessToken = Jwts.builder() .setSubject(userDetails.getUsername()) .setClaims(claims) .setIssuedAt(now2) .setExpiration(accessTokenExpire) .signWith(key,SignatureAlgorithm.HS256) .compact(); // RefreshToken 생성 Date refreshTokenExpire = new Date(now + this.refreshTokenTime); String refreshToken = Jwts.builder() .setIssuedAt(now2) .setClaims(claims) .setExpiration(refreshTokenExpire) .signWith(key, SignatureAlgorithm.HS256) .compact(); return TokenDTO.builder() .grantType("Bearer") .accessToken(accessToken) .refreshToken(refreshToken) .userEmail(userDetails.getUsername()) .build(); } // accessToken 생성 // 리프레시 토큰을 사용하여 새로운 액세스 토큰을 생성하는 로직을 구현 public TokenDTO createAccessToken(String refreshToken, List<GrantedAuthority> authorities) { Long now = (new Date()).getTime(); Date now2 = new Date(); Date accessTokenExpire = new Date(now + this.accessTokenTime); String userEmail = extractUserEmailFromToken(refreshToken); String accessToken = Jwts.builder() .setIssuedAt(now2) .setSubject(userEmail) .setExpiration(accessTokenExpire) .claim(AUTHORITIES_KEY, authorities ) .signWith(key, SignatureAlgorithm.HS256) .compact(); log.info("accessToken : " + accessToken); return TokenDTO.builder() .grantType("Bearer ") .accessToken(accessToken) .userEmail(userEmail) .build(); } // 리프레시 토큰의 유효성을 검증하는 로직을 구현 // 예를 들어, 토큰 서명 검증 및 만료 시간 확인 등을 수행 public boolean validateRefreshToken(String refreshToken) { try { // 토큰의 유효성을 검증하는 로직을 구현 // 예를 들어, 토큰의 서명을 확인하고 만료 시간을 검사합니다. // 유효한 토큰인 경우 true를 반환하고, 그렇지 않은 경우 false를 반환합니다. Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(refreshToken); return true; } catch (Exception e) { return false; } } // 리프레시 토큰에서 사용자 이메일을 추출하는 로직을 구현 // 예를 들어, 토큰의 특정 클레임에서 사용자 이메일을 추출하여 반환 public String extractUserEmailFromToken(String refreshToken) { Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(refreshToken).getBody(); // 사용자 이메일을 추출하는 로직을 구현하여 결과를 반환 return claims.getSubject(); } // JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 코드 // 토큰으로 클레임을 만들고 이를 이용해 유저 객체를 만들어서 최종적으로 authentication 객체를 리턴 // 인증 정보 조회 public Authentication getAuthentication(String token) { // 토큰 복호화 메소드 Claims claims = parseClaims(token); if(claims.get("auth") == null) { throw new RuntimeException("권한 정보가 없는 토큰입니다."); } // 클레임 권한 정보 가져오기 Collection<? extends GrantedAuthority> authorities = Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(",")) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); // UserDetails 객체를 만들어서 Authentication 리턴 User principal = new User(claims.getSubject(), "", authorities); return new UsernamePasswordAuthenticationToken(principal, token, authorities); } private Claims parseClaims(String token) { try { return Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); } catch (ExpiredJwtException e) { log.info("ExpiredJwtException : " + e.getMessage()); log.info("ExpiredJwtException : " + e.getClaims()); return e.getClaims(); } } // 토큰의 유효성 검증을 수행 public boolean validateToken(String token) { try { Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token); return true; } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { log.info("잘못된 JWT 서명입니다."); } catch (ExpiredJwtException e) { log.info("만료된 JWT 토큰입니다."); } catch (UnsupportedJwtException e) { log.info("지원되지 않는 JWT 토큰입니다."); }catch (IllegalArgumentException e) { log.info("JWT 토큰이 잘못되었습니다."); } return false; } }JwtAuthenticationFilterpackage com.example.project1.config.jwt; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.StringUtils; import org.springframework.web.filter.GenericFilterBean; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; // 클라이언트 요청 시 JWT 인증을 하기 위해 설치하는 커스텀 필터로 // UsernamePasswordAuthenticationFiler 이전에 실행된다. // 이전에 실행된다는 뜻은 JwtAuthenticationFilter 를 통과하면 // UsernamePasswordAuthenticationFilter 이후의 필터는 통과한 것으로 본다는 뜻이다. // 쉽게 말해서, Username + Password 를 통한 인증을 Jwt 를 통해 수행한다는 것이다. // JWT 방식은 세션과 다르게 Filter 하나를 추가해야 합니다. // 이제 사용자가 로그인을 했을 때, Request 에 가지고 있는 Token 을 해석해주는 로직이 필요합니다. // 이 역할을 해주는것이 JwtAuthenticationFilter입니다. // 세부 비즈니스 로직들은 TokenProvider에 적어둡니다. 일종의 service 클래스라고 생각하면 편합니다. // 1. 사용자의 Request Header에 토큰을 가져옵니다. // 2. 해당 토큰의 유효성 검사를 실시하고 유효하면 // 3. Authentication 인증 객체를 만들고 // 4. ContextHolder에 저장해줍니다. // 5. 해당 Filter 과정이 끝나면 이제 시큐리티에 다음 Filter로 이동하게 됩니다. @RequiredArgsConstructor @Slf4j public class JwtAuthenticationFilter extends GenericFilterBean { public static final String HEADER_AUTHORIZATION = "Authorization"; private final JwtProvider jwtProvider; // doFilter는 토큰의 인증정보를 SecurityContext에 저장하는 역할 수행 @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; // Request Header에서 JWT 토큰을 추출 // 요청 헤더에서 JWT 토큰을 추출하는 역할 String jwt = resolveToken(httpServletRequest); String requestURI = httpServletRequest.getRequestURI(); if(StringUtils.hasText(jwt) && jwtProvider.validateToken(jwt)){ // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext에 저장 Authentication authentication = jwtProvider.getAuthentication(jwt); SecurityContextHolder.getContext().setAuthentication(authentication); log.info("Security Context에 '{}' 인증 정보를 저장했습니다., uri : {}", authentication.getName(), requestURI); } else { log.debug("유효한 JWT 토큰이 없습니다. uri : {}", requestURI); } chain.doFilter(request, response); } // Request Header 에서 토큰 정보를 꺼내오기 위한 메소드 // HEADER_AUTHORIZATION로 정의된 헤더 이름을 사용하여 토큰을 찾고, // 토큰이 "Bearer "로 시작하는 경우에만 실제 토큰 값을 반환 private String resolveToken(HttpServletRequest httpServletRequest) { String bearerToken = httpServletRequest.getHeader(HEADER_AUTHORIZATION); if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } else { return null; } } }JwtAccessDeniedHandler, JwtAuthenticationEntryPoint 생략 PrincipalDetailspackage com.example.project1.config.auth; import com.example.project1.entity.member.MemberEntity; import lombok.Getter; import lombok.Setter; import lombok.ToString; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.core.user.OAuth2User; import java.util.ArrayList; import java.util.Collection; import java.util.Map; @Setter @Getter @ToString public class PrincipalDetails implements UserDetails, OAuth2User { private MemberEntity member; private Map<String, Object> attributes; // 일반 로그인 public PrincipalDetails(MemberEntity member) { this.member = member; } // OAuth2 로그인 public PrincipalDetails(MemberEntity member, Map<String, Object> attributes) { this.member = member; this.attributes = attributes; } // 해당 유저의 권한을 리턴하는 곳 @Override public Collection<? extends GrantedAuthority> getAuthorities() { Collection<GrantedAuthority> collection = new ArrayList<>(); collection.add(new SimpleGrantedAuthority("ROLE_" + member.getUserType().toString())); return collection; } // 사용자 패스워드를 반환 @Override public String getPassword() { return member.getUserPw(); } // 사용자 이름 반환 @Override public String getUsername() { return member.getUserEmail(); } // 계정 만료 여부 반환 @Override public boolean isAccountNonExpired() { // 만료되었는지 확인하는 로직 // true = 만료되지 않음 return true; } // 계정 잠금 여부 반환 @Override public boolean isAccountNonLocked() { // true = 잠금되지 않음 return true; } // 패스워드의 만료 여부 반환 @Override public boolean isCredentialsNonExpired() { // 패스워드가 만료되었는지 확인하는 로직 // true = 만료되지 않음 return true; } // 계정 사용 가능 여부 반환 @Override public boolean isEnabled() { // 계정이 사용 가능한지 확인하는 로직 // true = 사용 가능 return true; } @Override public Map<String, Object> getAttributes() { return attributes; } @Override public String getName() { return null; } }PrincipalDetailsServicepackage com.example.project1.config.auth; import com.example.project1.entity.member.MemberEntity; import com.example.project1.repository.member.MemberRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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; // http://localhost:8080/login ← 이 때 동작을 함 @Service @RequiredArgsConstructor @Slf4j public class PrincipalDetailsService implements UserDetailsService { private MemberRepository memberRepository; // 시큐리티 session = Authentication = UserDetails // 함수 종료시 @AuthenticationPrincipal 어노테이션이 만들어진다. @Override public UserDetails loadUserByUsername(String userEmail) throws UsernameNotFoundException { MemberEntity member = memberRepository.findByUserEmail(userEmail); log.info("user : " + member); return new PrincipalDetails(member); } }OAuth2UserInfopackage com.example.project1.config.oauth2.provider; public interface OAuth2UserInfo { String getProviderId(); String getProvider(); String getEmail(); String getName(); }PrincipalOauth2UserServicepackage com.example.project1.config.oauth2; import com.example.project1.config.auth.PrincipalDetails; import com.example.project1.config.oauth2.provider.GoogleUserInfo; import com.example.project1.config.oauth2.provider.NaverUserInfo; import com.example.project1.config.oauth2.provider.OAuth2UserInfo; import com.example.project1.domain.member.MemberDTO; import com.example.project1.domain.member.UserType; import com.example.project1.entity.member.MemberEntity; import com.example.project1.repository.member.MemberRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; 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; import java.util.Objects; import java.util.Optional; @Service @Slf4j @RequiredArgsConstructor public class PrincipalOauth2UserService extends DefaultOAuth2UserService { private final BCryptPasswordEncoder bCryptPasswordEncoder; private final MemberRepository memberRepository; // 구글로부터 받은 userReuest 데이터에 대한 후처리되는 함수 @Override public PrincipalDetails loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { // registrationId로 어떤 OAuth로 로그인 했는지 확인가능 log.info("clientRegistration : " + userRequest.getClientRegistration() ); log.info("accessToken : " + userRequest.getAccessToken().getTokenValue() ); OAuth2User oAuth2User = super.loadUser(userRequest); // 구글 로그인 버튼 클릭 →구글 로그인 창 → 로그인 완료 → code 를 리턴(OAuth-Client 라이브러리) → AccessToken 요청 // userRequest 정보 → 회원 프로필 받아야함(loadUser 함수 호출) → 구글로부터 회원 프로필을 받아준다. log.info("getAttributes : " + oAuth2User.getAttributes()); // 회원가입을 강제로 진행 OAuth2UserInfo oAuth2UserInfo = null; if(userRequest.getClientRegistration().getRegistrationId().equals("google")) { log.info("구글 로그인 요청"); oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes()); } else if(userRequest.getClientRegistration().getRegistrationId().equals("naver")) { log.info("네이버 로그인 요청"); // 네이버는 response를 json으로 리턴을 해주는데 아래의 코드가 받아오는 코드다. // response={id=5SN-ML41CuX_iAUFH6-KWbuei8kRV9aTHdXOOXgL2K0, email=zxzz8014@naver.com, name=전혜영} // 위의 정보를 NaverUserInfo에 넘기면 oAuth2UserInfo = new NaverUserInfo((Map)oAuth2User.getAttributes().get("response")); } else { log.info("구글과 네이버만 지원합니다."); } String provider = oAuth2UserInfo.getProvider(); String providerId = oAuth2UserInfo.getProviderId(); // 예) google_109742856182916427686 String userName = provider + "_" + providerId; String password = bCryptPasswordEncoder.encode("get"); String email = oAuth2UserInfo.getEmail(); UserType role = UserType.USER; MemberEntity member = memberRepository.findByUserEmail(email); if(member == null) { log.info("OAuth 로그인이 최초입니다."); member = MemberEntity.builder() .userName(userName) .userPw(password) .userEmail(email) .userType(role) .provider(provider) .providerId(providerId) .build(); memberRepository.save(member); } else { log.info("로그인을 이미 한적이 있습니다. 당신은 자동회원가입이 되어 있습니다."); } return new PrincipalDetails(member, oAuth2User.getAttributes()); } }GoogleUserInfopackage com.example.project1.config.oauth2.provider; import java.util.Map; public class GoogleUserInfo implements OAuth2UserInfo{ // getAttributes()를 받음 private Map<String, Object> attributes; public GoogleUserInfo(Map<String, Object> attributes) { this.attributes = attributes; } @Override public String getProviderId() { return (String) attributes.get("sub"); } @Override public String getProvider() { return "google"; } @Override public String getEmail() { return (String) attributes.get("email"); } @Override public String getName() { return (String) attributes.get("name"); } }NaverUserInfopackage com.example.project1.config.oauth2.provider; import java.util.Map; public class NaverUserInfo implements OAuth2UserInfo{ // oauth2User.getAttributes()를 받음 private Map<String,Object> attributes; // PrincipalOauth2UserService에서 new NaverUserInfo((Map)oAuth2User.getAttributes().get("response"))로 // Oauth2 네이버 로그인 정보를 받아온다. // → {id=5SN-ML41CuX_iAUFH6-KWbuei8kRV9aTHdXOOXgL2K0, email=zxzz8014@naver.com, name=전혜영} public NaverUserInfo(Map<String, Object> attributes) { this.attributes = attributes; } @Override public String getProviderId() { return (String)attributes.get("id"); } @Override public String getProvider() { return "naver"; } @Override public String getEmail() { return (String)attributes.get("email"); } @Override public String getName() { return (String)attributes.get("name"); } } MemberController @GetMapping("/success-oauth") public ResponseEntity<?> createTokenForGoogle(@AuthenticationPrincipal OAuth2User oAuth2User) { if(oAuth2User == null) { log.info("받아올 정보가 없습니다 ㅠㅠ"); return ResponseEntity.status(HttpStatus.NOT_FOUND).body("정보가 없어...."); } else { log.info("oauth2User 정보를 받아오자 : " + oAuth2User); // OAuth2User에서 필요한 정보를 추출하여 UserDetails 객체를 생성합니다. ResponseEntity<TokenDTO> token = memberService.createToken(oAuth2User); log.info("token : " + token); return ResponseEntity.ok().body(token); } }TokenDTOpackage com.example.project1.domain.jwt; import com.example.project1.entity.jwt.TokenEntity; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; import java.util.Date; @Getter @ToString @NoArgsConstructor public class TokenDTO { private Long id; // JWT 대한 인증 타입, 여기서는 Bearer를 사용하고 // 이후 HTTP 헤더에 prefix로 붙여주는 타입 private String grantType; private String accessToken; private String refreshToken; private String userEmail; private String nickName; private Long userId; private Date accessTokenTime; private Date refreshTokenTime; @Builder public TokenDTO(String grantType, String accessToken, String refreshToken, String userEmail, String nickName, Long userId, Date accessTokenTime, Date refreshTokenTime) { this.grantType = grantType; this.accessToken = accessToken; this.refreshToken = refreshToken; this.userEmail = userEmail; this.nickName = nickName; this.userId = userId; this.accessTokenTime = accessTokenTime; this.refreshTokenTime = refreshTokenTime; } public static TokenDTO toTokenDTO(TokenEntity tokenEntity) { TokenDTO tokenDTO = TokenDTO.builder() .grantType(tokenEntity.getGrantType()) .accessToken(tokenEntity.getAccessToken()) .refreshToken(tokenEntity.getRefreshToken()) .userEmail(tokenEntity.getUserEmail()) .nickName(tokenEntity.getNickName()) .userId(tokenEntity.getId()) .accessTokenTime(tokenEntity.getAccessTokenTime()) .refreshTokenTime(tokenEntity.getRefreshTokenTime()) .build(); return tokenDTO; } }TokenEntitypackage com.example.project1.entity.jwt; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import java.util.Date; @Entity @Getter @NoArgsConstructor @ToString public class TokenEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String grantType; private String accessToken; private String refreshToken; private String userEmail; private String nickName; private Long userId; private Date accessTokenTime; private Date refreshTokenTime; @Builder public TokenEntity(Long id, String grantType, String accessToken, String refreshToken, String userEmail, String nickName, Long userId, Date accessTokenTime, Date refreshTokenTime) { this.id = id; this.grantType = grantType; this.accessToken = accessToken; this.refreshToken = refreshToken; this.userEmail = userEmail; this.nickName = nickName; this.userId = userId; this.accessTokenTime = accessTokenTime; this.refreshTokenTime = refreshTokenTime; } }MemberServicepackage com.example.project1.service.member; import com.example.project1.config.jwt.JwtAuthenticationFilter; import com.example.project1.config.jwt.JwtProvider; import com.example.project1.domain.jwt.TokenDTO; import com.example.project1.domain.member.MemberDTO; import com.example.project1.domain.member.UserType; import com.example.project1.entity.jwt.TokenEntity; import com.example.project1.entity.member.MemberEntity; import com.example.project1.repository.jwt.TokenRepository; import com.example.project1.repository.member.MemberRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import java.util.*; @Service @RequiredArgsConstructor @Slf4j public class MemberService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; private final AuthenticationManagerBuilder authenticationManagerBuilder; private final JwtProvider jwtProvider; private final TokenRepository tokenRepository; // 회원가입 public String signUp(MemberDTO memberDTO) throws Exception { try { MemberEntity byUserEmail = memberRepository.findByUserEmail(memberDTO.getUserEmail()); if (byUserEmail != null) { return "이미 가입된 회원입니다."; } // 아이디가 없다면 DB에 넣어서 등록 해준다. MemberEntity member = MemberEntity.builder() .userEmail(memberDTO.getUserEmail()) .userPw(passwordEncoder.encode(memberDTO.getUserPw())) .userName(memberDTO.getUserName()) .nickName(memberDTO.getNickName()) .userType(memberDTO.getUserType()) .provider(memberDTO.getProvider()) .providerId(memberDTO.getProviderId()) .build(); log.info("member : " + member); MemberEntity save = memberRepository.save(member); // MemberDTO memberDTO1 = MemberDTO.toMemberDTO(Optional.of(save)); return "회원가입에 성공했습니다."; } catch (Exception e) { log.error(e.getMessage()); throw e; // 예외를 던져서 예외 처리를 컨트롤러로 전달 } } // 아이디 조회 public MemberDTO search(Long userId) { Optional<MemberEntity> searchId = memberRepository.findById(userId); MemberDTO memberDTO = MemberDTO.toMemberDTO(searchId); return memberDTO; } // 로그인 public ResponseEntity<TokenDTO> login(String userEmail, String userPw) throws Exception { MemberEntity findUser = memberRepository.findByUserEmail(userEmail); log.info("findUser : " + findUser); if (findUser != null) { Authentication authentication = new UsernamePasswordAuthenticationToken(userEmail, userPw); TokenDTO token = jwtProvider.createToken(authentication); // // Login ID/PW를 기반으로 UsernamePasswordAuthenticationToken 생성 token = TokenDTO.builder() .grantType(token.getGrantType()) .accessToken(token.getAccessToken()) .refreshToken(token.getRefreshToken()) .userEmail(findUser.getUserEmail()) .nickName(findUser.getNickName()) .userId(findUser.getUserId()) .build(); TokenEntity tokenEntity = TokenEntity.builder() .id(token.getId()) .grantType(token.getGrantType()) .accessToken(token.getAccessToken()) .refreshToken(token.getRefreshToken()) .userEmail(token.getUserEmail()) .nickName(token.getNickName()) .userId(token.getUserId()) .build(); log.info("token : " + tokenEntity); tokenRepository.save(tokenEntity); return new ResponseEntity<>(token, HttpStatus.OK); } else { return null; } } // 회원정보 수정 public MemberDTO update(MemberDTO memberDTO) { MemberEntity member = MemberEntity.builder() .userEmail(memberDTO.getUserEmail()) .userPw(passwordEncoder.encode(memberDTO.getUserPw())) .userName(memberDTO.getUserName()) .nickName(memberDTO.getNickName()) .userType(memberDTO.getUserType()) .provider(memberDTO.getProvider()) .providerId(memberDTO.getProviderId()) .build(); memberRepository.save(member); // 제대로 DTO 값이 엔티티에 넣어졌는지 확인하기 위해서 // 엔티티에 넣어주고 다시 DTO 객체로 바꿔서 리턴을 해줬습니다. MemberDTO memberDto = MemberDTO.toMemberDTO(Optional.of(member)); log.info("memberDto : " + memberDto); return memberDto; } // 소셜 로그인 성공시 jwt 반환 // OAuth2User에서 필요한 정보를 추출하여 UserDetails 객체를 생성하는 메서드 public ResponseEntity<TokenDTO> createToken(OAuth2User oAuth2User) { String userEmail = oAuth2User.getAttribute("email"); log.info("userEmail : " + userEmail); MemberEntity findMember = memberRepository.findByUserEmail(userEmail); // 권한 정보 추출 List<GrantedAuthority> authorities = getAuthoritiesForUser(findMember); // UserDetails 객체 생성 (사용자의 아이디 정보를 활용) // 첫 번째 인자 : username 사용자 아이디 // 두 번째 인자 : 사용자의 비밀번호 // 세 번째 인자 : 사용자의 권한 정보를 담은 컬렉션 UserDetails userDetails = new User(userEmail, null, authorities); log.info("userDetails : " + userDetails); TokenDTO token = jwtProvider.createToken2(userDetails); log.info("token : " + token); return ResponseEntity.ok().body(token); } private List<GrantedAuthority> getAuthoritiesForUser(MemberEntity member) { // 예시: 데이터베이스에서 사용자의 권한 정보를 조회하는 로직을 구현 // member 객체를 이용하여 데이터베이스에서 사용자의 권한 정보를 조회하는 예시로 대체합니다. UserType role = member.getUserType(); // 사용자의 권한 정보를 가져오는 로직 (예시) List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority(role.name())); return authorities; } } 이렇게 했는데 PrincipalOauth2UserService에는 값이 잘 받아지는데 컨트롤러에서 @AuthenticationPrincipal OAuth2User oAuth2User으로 소셜 로그인 성공하면 정보를 뽑아서 JWT를 발급해주려고 하는데 소셜 로그인을 하고 log를 찍어보면 null이 뜹니다.... 어떻게 해야할까요
-
해결됨Next.js 시작하기(feat. 지도 서비스 개발)
next/image 질문!
안녕하세요!궁금한게 있어서 질문드려요 ㅎㅎ현업에서 next version 12.3.4를 쓰고 있고next/image를 사용해서 이미지를 화면에 보여주려고 하고 있는데 일반 img 태그를 쓸 때보다 화질이 안좋습니다.. 어떤 부분을 개선해야 할까요?또한 next/image를 쓰면 이미지 최적화 및 lazy loading을 자동으로 지원한다고 했는데,priority를 설정 해주지 않으면 이미지가 늦게 로드 되고 lazy loading은 layout 옵션을 설정해주지 않으면 안되더라구요!구 버전은 자동으로 지원해주지 않는걸까요? 아래 처럼 쓰고 있습니다."next/future/image" "next/image" 두개 다 쓰고 있습니다! import Image from 'next/future/image'; <Image src="" alt="" width={320} height={395} quality={100} />
-
미해결실무자가 알려주는 CANoe - CAPL과 Panel 기본 사용법
CAPL panel 관련 질문입니다.
Toolbox에 기본적으로 지원하는 아이콘을 편집하거나 새롭게 만들수는 없나요???[예시]'Progress Bar'의 display 위치 중 'inside'를 추가'analog gauge'의 디자인 편집특정 Sys Variable의 변화그래프
-
미해결10주완성 C++ 코딩테스트 | 알고리즘 코딩테스트
8-x
강의를 3-4번을 봐도 이해가 안되는 부분이 있어서 질문드립니다.어떻게 2구간의 물이 3으로 맞춰 질수가 있나요...문제 예시를 보면,높이 4와 높이 1의 구명을 통해 바깥으로 물이 빠지고높이 3의 구멍으로 물이 들어와서 다시 높이 1의 구멍으로 바깥으로 물이나갈텐데 ... 2개의 높이가.. 3으로 어떻게 맞춰질수가.. 있나요?
-
미해결[2026년 출제기준] 웹디자인개발기능사 실기시험 완벽 가이드
선생님처럼 했는데 창이이상하게 그림이 계속 떨어지네요 ,,,
/*tab-inner*/ .tab-inner, .gallery-inner{ width: 95%; margin: auto; } .tab-inner .btn{} .tab-inner .btn span, .gallery-inner .btn span{ border: 1px solid #000; display: inline-block; width: 100px; padding: 5px; text-align: center; border-radius: 5px 5px 0 0; border-bottom: none; margin-bottom: -1px; background-color: #fff; } .tab-inner .tab, .gallery-inner .tab{ border: 1px solid #000; padding: 0 10px; height: 155px; } .tab-inner .tab a { display: block; text-decoration: none; color: #000; border-bottom: 1px solid #333; padding: 4px; } .tab-inner .tab a:last-child{ border-bottom: none; } .tab-inner .tab a b { float: right; font-weight: normal; } .gallery .tab img{ width: 100px; } .gallery .tab{ text-align: center; padding-top: 30px; box-sizing: border-box; }소스는 똑같이한거같은데 사진이 문제인가 저렇케 내려와서요 뭐가 문제일까요 /...???