<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>동동이개발바닥</title>
    <link>https://devhan.tistory.com/</link>
    <description>어차피 할 거면 긍정적으로 하고 싶은 개발자</description>
    <language>ko</language>
    <pubDate>Fri, 26 Jun 2026 20:46:23 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>데부한</managingEditor>
    <image>
      <title>동동이개발바닥</title>
      <url>https://tistory1.daumcdn.net/tistory/4771792/attach/9075f69cab144f2e85ce6f0acbf7166d</url>
      <link>https://devhan.tistory.com</link>
    </image>
    <item>
      <title>[토이 프로젝트] spring boot 회원가입 기능 -2</title>
      <link>https://devhan.tistory.com/358</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이전 게시글&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741696329147&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[토이 프로젝트] spring boot 스프링 시큐리티 토큰 적용하기 (Bearer Token)&quot; data-og-description=&quot;이전 글&amp;nbsp;[토이 프로젝트] spring boot 로그인 기능 만들기-2 (스프링 시큐리티 적용)이전 프로젝트&amp;nbsp;[토이 프로젝트] 회원가입 기능 만들기-1사실 아직 만들고 싶은 프로젝트는 생각안해봤다.다만 프&quot; data-og-host=&quot;devhan.tistory.com&quot; data-og-source-url=&quot;https://devhan.tistory.com/357&quot; data-og-url=&quot;https://devhan.tistory.com/357&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/daTPjc/hyYqZt9vW8/iNRYbh25idRn4UdiKwA7Tk/img.png?width=390&amp;amp;height=274&amp;amp;face=0_0_390_274,https://scrap.kakaocdn.net/dn/ck9eYj/hyYqK4Po8g/50lBrXkIBajxbJ2lzC7s9K/img.png?width=390&amp;amp;height=274&amp;amp;face=0_0_390_274,https://scrap.kakaocdn.net/dn/6ZLie/hyYmZvM63O/7Kp4nmnjptu0XUQFvNAcg1/img.png?width=1231&amp;amp;height=652&amp;amp;face=0_0_1231_652&quot;&gt;&lt;a href=&quot;https://devhan.tistory.com/357&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devhan.tistory.com/357&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/daTPjc/hyYqZt9vW8/iNRYbh25idRn4UdiKwA7Tk/img.png?width=390&amp;amp;height=274&amp;amp;face=0_0_390_274,https://scrap.kakaocdn.net/dn/ck9eYj/hyYqK4Po8g/50lBrXkIBajxbJ2lzC7s9K/img.png?width=390&amp;amp;height=274&amp;amp;face=0_0_390_274,https://scrap.kakaocdn.net/dn/6ZLie/hyYmZvM63O/7Kp4nmnjptu0XUQFvNAcg1/img.png?width=1231&amp;amp;height=652&amp;amp;face=0_0_1231_652');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[토이 프로젝트] spring boot 스프링 시큐리티 토큰 적용하기 (Bearer Token)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이전 글&amp;nbsp;[토이 프로젝트] spring boot 로그인 기능 만들기-2 (스프링 시큐리티 적용)이전 프로젝트&amp;nbsp;[토이 프로젝트] 회원가입 기능 만들기-1사실 아직 만들고 싶은 프로젝트는 생각안해봤다.다만 프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devhan.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 HttpOnly Cookie + JWT 조합을 구현해보려고 했으나.. 회원가입 기능과 테스트 쪽을 먼저 구현해야 할 것 같아서 노선을 틀었다. 그래서 이번에 할 목록은 아래 4개다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원가입 기능 강화&lt;/li&gt;
&lt;li&gt;로그아웃 기능&lt;/li&gt;
&lt;li&gt;AuthController 리팩토링&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 게시글 제목은 회원가입 기능-2지만 더 다양한 걸 한 번에 쭉 해볼 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가 제일 금방할 것 같은 로그아웃 기능을 먼저 만들어보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;logout 기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 로그아웃 기능을 화면에서 만질 수 있게 바꿔보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;index.html&lt;/h3&gt;
&lt;pre id=&quot;code_1741813295047&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;메인&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
        .hidden {
            display: none;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h2&amp;gt;메인입니다.&amp;lt;/h2&amp;gt;
    &amp;lt;a href=&quot;member/login.html&quot;&amp;gt;로그인 이동&amp;lt;/a&amp;gt;
    &amp;lt;a href=&quot;member/signup.html&quot;&amp;gt;회원가입 이동&amp;lt;/a&amp;gt;
    &amp;lt;a href=&quot;test/test.html&quot;&amp;gt;인증 필요 화면&amp;lt;/a&amp;gt;
    &amp;lt;button id=&quot;btnLogout&quot;&amp;gt;로그아웃&amp;lt;/button&amp;gt;

    &amp;lt;script&amp;gt;
        window.onload = function() {
            const token = localStorage.getItem(&quot;token&quot;);
            if(!token) {
                document.getElementById(&quot;btnLogout&quot;).classList.add(&quot;hidden&quot;);
            }
        }

        document.getElementById(&quot;btnLogout&quot;).addEventListener(&quot;click&quot;, function(){
            logout();
        });

        async function logout() {
            const token = localStorage.getItem(&quot;token&quot;);

            try {
                const response = await fetch('api/auth/logout', {
                    method: 'POST',
                    headers: {
                        &quot;Authorization&quot; : `Bearer ${token}`,
                        &quot;Content-Type&quot; : &quot;application/json&quot;
                    }
                });

                if(!response.ok) throw new Error(&quot;로그아웃이 실패했습니다.&quot;);

                alert(&quot;로그아웃 성공&quot;);
				window.location.replace(window.location.href);
            } catch (error) {
                console.error(error);
                alert(&quot;로그아웃 실패&quot;);
            }
        }
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 진입 시&amp;nbsp; token 값이 있으면 로그아웃 버튼을 숨기도록 했다. 테스트해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zIlzb/btsMIFWYm51/ASUlIEKos0k1LzCQ6m06Ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zIlzb/btsMIFWYm51/ASUlIEKos0k1LzCQ6m06Ek/img.png&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;181&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.3649%; margin-right: 10px;&quot; data-widthpercent=&quot;49.95&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zIlzb/btsMIFWYm51/ASUlIEKos0k1LzCQ6m06Ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzIlzb%2FbtsMIFWYm51%2FASUlIEKos0k1LzCQ6m06Ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;392&quot; height=&quot;181&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wlkEq/btsMJctl0cI/kidKdgq0oHNHZj945OxrVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wlkEq/btsMJctl0cI/kidKdgq0oHNHZj945OxrVk/img.png&quot; data-origin-width=&quot;382&quot; data-origin-height=&quot;176&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4723%;&quot; data-widthpercent=&quot;50.05&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wlkEq/btsMJctl0cI/kidKdgq0oHNHZj945OxrVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwlkEq%2FbtsMJctl0cI%2FkidKdgq0oHNHZj945OxrVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;382&quot; height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인하지 않았을 경우 로그아웃 버튼이 보이지 않고, 로그인하면 로그아웃 버튼이 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 서버에 logout 로직을 만들어주자.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;AuthController&lt;/h3&gt;
&lt;pre id=&quot;code_1741866507179&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;logout&quot;)
public ResponseEntity&amp;lt;?&amp;gt; logout(@RequestHeader(&quot;Authorization&quot;) String token) {
   if(token.startsWith(&quot;Bearer &quot;)) {
       token = token.replace(&quot;Bearer &quot;, &quot;&quot;);
   }

   authService.logout(token);
   return ResponseEntity.ok().body(&quot;로그아웃 성공&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Token인지 확인하기 위해 Bearer 문자열 검사를 하고, 없애준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;AuthService&lt;/h3&gt;
&lt;pre id=&quot;code_1741866556789&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void logout(String token) {
    if(validateToken(token)) {
        tokenStore.remove(token);
    } else {
        throw new IllegalArgumentException(&quot;유효하지 않은 토큰입니다.&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;validationToken 메서드를 이용해서 토큰 저장소에 해당 토큰이 있나 검사하고 있으면 토큰 저장소에서 삭제하고 토큰 저장소가 없으면 에러를 뱉는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 해보자! 로그아웃해도 메인에 여전히 로그아웃 버튼이 남아있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;165&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wOdyG/btsMKHZTnDY/2URKL2YHjsJsmOvZZwJtlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wOdyG/btsMKHZTnDY/2URKL2YHjsJsmOvZZwJtlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wOdyG/btsMKHZTnDY/2URKL2YHjsJsmOvZZwJtlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwOdyG%2FbtsMKHZTnDY%2F2URKL2YHjsJsmOvZZwJtlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;388&quot; height=&quot;165&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;165&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보니까 로그아웃 버튼의 display 속성 값을 localStorage에 toekn이 있나 없나 검사하기 때문에 로그아웃 시 localStorage의 token 값도 삭제해줘야할 것 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1741867010871&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;alert(&quot;로그아웃 성공&quot;);
localStorage.removeItem(&quot;token&quot;);
window.location.replace(window.location.href);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 로그인하고 로그아웃 해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGWY1M/btsMJ33towH/kJgN63CHZIBwVZeTQPD660/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGWY1M/btsMJ33towH/kJgN63CHZIBwVZeTQPD660/img.png&quot; data-origin-width=&quot;364&quot; data-origin-height=&quot;163&quot; data-is-animation=&quot;false&quot; style=&quot;width: 50.6529%; margin-right: 10px;&quot; data-widthpercent=&quot;51.25&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGWY1M/btsMJ33towH/kJgN63CHZIBwVZeTQPD660/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGWY1M%2FbtsMJ33towH%2FkJgN63CHZIBwVZeTQPD660%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;364&quot; height=&quot;163&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbwJmU/btsMKpZiaqw/ku7PMeo5TrZ1AL0OIrynDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbwJmU/btsMKpZiaqw/ku7PMeo5TrZ1AL0OIrynDK/img.png&quot; data-origin-width=&quot;376&quot; data-origin-height=&quot;177&quot; data-is-animation=&quot;false&quot; style=&quot;width: 48.1843%;&quot; data-widthpercent=&quot;48.75&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbwJmU/btsMKpZiaqw/ku7PMeo5TrZ1AL0OIrynDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbwJmU%2FbtsMKpZiaqw%2Fku7PMeo5TrZ1AL0OIrynDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;376&quot; height=&quot;177&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 되는 걸 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Controller 리팩토링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 AuthController에 되~~~게 거슬렸던 Response 타입을 신경써서 바꿔보자. 지금 Response 타입은 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1741869113403&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public ResponseEntity&amp;lt;?&amp;gt; validateToken(...) {
	// ..
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 ResponseEntity&amp;lt;?&amp;gt;로 되어있다. 예전에 영한쓰 자바 강의를 들었을 때 ?라는 와일드 카드는 되도록 사용하지 않는 것이 좋다고 했다. 왜냐면 어떤 타입이든 다 들어갈 수 있어서 어떤 타입이 반환될지 예측하기 어렵고 유지보수가 복잡하기 떄문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthController에서 반환 타입은 내가 만든 DTO 아니면 ResponseEntity 객체에 http status 코드나 메세지를 담은 객체이므로&amp;nbsp; ApiResponse용&amp;nbsp; DTO를 하나 더 만들어 ApiResponse에 응답 데이터와 http status 코드 등을 넣어 유지보수 하기 쉽도록 변경해보려한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ApiResponse&lt;/h3&gt;
&lt;pre id=&quot;code_1741870850149&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@AllArgsConstructor
public class ApiResponse&amp;lt;T&amp;gt; {
    private int status;     // HTTP 상태 코드
    private String message; // 응답 메세지
    private T data;         // 데이터
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 만들고 하나의 api..회원가입의 코드를 변경하고 테스트 해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1741871436406&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;signup&quot;)
    public ResponseEntity&amp;lt;ApiResponse&amp;lt;Boolean&amp;gt;&amp;gt; signup(@RequestBody SignupRequestDto signupRequestDto) {
        boolean isSuccess = authService.signup(signupRequestDto);
        HttpStatus status = isSuccess ? HttpStatus.OK : HttpStatus.BAD_REQUEST;
        String message = isSuccess ? &quot;회원 가입 성공&quot; : &quot;회원 가입 실패&quot;;

        return ResponseEntity.status(status).body(new ApiResponse&amp;lt;&amp;gt;(status.value(), message, isSuccess));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0j08x/btsMKfvTStC/Aufj1CxnxZg6kSNMNoTMt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0j08x/btsMKfvTStC/Aufj1CxnxZg6kSNMNoTMt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0j08x/btsMKfvTStC/Aufj1CxnxZg6kSNMNoTMt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0j08x%2FbtsMKfvTStC%2FAufj1CxnxZg6kSNMNoTMt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;626&quot; height=&quot;292&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 나온다. 근데 컨트롤러에서 ResponseEntity를 반환하는 부분을 뭔가 더 편하게 바꿔줄 수 있을 거 같아 ApiResponse에 정적 메서드를 추가했다.&lt;/p&gt;
&lt;pre id=&quot;code_1741902342843&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@AllArgsConstructor
public class ApiResponse&amp;lt;T&amp;gt; {
    private final int status;     // HTTP 상태 코드
    private final String message; // 응답 메세지
    private final T data;         // 데이터

    public static &amp;lt;T&amp;gt; ApiResponse&amp;lt;T&amp;gt; success(T data) {
        return new ApiResponse&amp;lt;&amp;gt;(HttpStatus.OK.value(), &quot;요청을 성공적으로 처리했습니다.&quot;, data);
    }

    public static &amp;lt;T&amp;gt; ApiResponse&amp;lt;T&amp;gt; success(String message, T data) {
        return new ApiResponse&amp;lt;&amp;gt;(HttpStatus.OK.value(), message, data);
    }

    public static &amp;lt;T&amp;gt; ApiResponse&amp;lt;T&amp;gt; error(HttpStatus status, String message) {
        return new ApiResponse&amp;lt;&amp;gt;(status.value(), message, null);
    }

    public static &amp;lt;T&amp;gt; ApiResponse&amp;lt;T&amp;gt; error(HttpStatus status, String message,T data) {
        return new ApiResponse&amp;lt;&amp;gt;(status.value(), message, data);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정적 메서드를 이용해서 컨트롤러 반환 부분을 다시 바꿔 주자.&lt;/p&gt;
&lt;pre id=&quot;code_1741902513976&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;signup&quot;)
public ResponseEntity&amp;lt;ApiResponse&amp;lt;Boolean&amp;gt;&amp;gt; signup(@RequestBody SignupRequestDto signupRequestDto) {
    boolean isSuccess = authService.signup(signupRequestDto);

    return isSuccess ?
            ResponseEntity.ok(ApiResponse.success(true)) :
            ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .body(ApiResponse.error(HttpStatus.BAD_REQUEST, &quot;회원 가입 실패&quot;, false));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 컨트롤러의 모든 메서드들을 바꿔주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1741902973646&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;signup&quot;)
public ResponseEntity&amp;lt;ApiResponse&amp;lt;Boolean&amp;gt;&amp;gt; signup(@RequestBody SignupRequestDto signupRequestDto) {
    boolean isSuccess = authService.signup(signupRequestDto);

    return isSuccess ?
            ResponseEntity.ok(ApiResponse.success(true)) :
            ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .body(ApiResponse.error(HttpStatus.BAD_REQUEST, &quot;회원 가입 실패&quot;, false));
}

@PostMapping(&quot;login&quot;)
public ResponseEntity&amp;lt;ApiResponse&amp;lt;LoginResponseDto&amp;gt;&amp;gt; login(@RequestBody LoginRequestDto loginRequestDto) {
    LoginResponseDto loginResponseDto = authService.login(loginRequestDto);

    return loginResponseDto.getToken() != null ?
            ResponseEntity.ok(ApiResponse.success(&quot;로그인 성공&quot;, loginResponseDto)) :
            ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ApiResponse.error(HttpStatus.UNAUTHORIZED, &quot;아이디와 비밀번호를 확인해주세요.&quot;));
}

@GetMapping(&quot;validate&quot;)
public ResponseEntity&amp;lt;ApiResponse&amp;lt;String&amp;gt;&amp;gt; validateToken(@RequestHeader(&quot;Authorization&quot;) String authorizationHeader) {

    // Authorization 헤더가 없으면 401 반환
    if(authorizationHeader == null || !authorizationHeader.startsWith(&quot;Bearer &quot;)) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(ApiResponse.error(HttpStatus.UNAUTHORIZED, &quot;인증 필요&quot;));
    }

    String token = authorizationHeader.replace(&quot;Bearer &quot;, &quot;&quot;);
    boolean isValid = authService.validateToken(token);

    return isValid ?
            ResponseEntity.ok(ApiResponse.success(&quot;유효한 토큰&quot;)) :
            ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(ApiResponse.error(HttpStatus.UNAUTHORIZED, &quot;인증 필요&quot;));
}

@PostMapping(&quot;logout&quot;)
public ResponseEntity&amp;lt;ApiResponse&amp;lt;String&amp;gt;&amp;gt; logout(@RequestHeader(&quot;Authorization&quot;) String token) {
   if(token.startsWith(&quot;Bearer &quot;)) {
       token = token.replace(&quot;Bearer &quot;, &quot;&quot;);
   }

   authService.logout(token);
   return ResponseEntity.ok(ApiResponse.success(&quot;로그아웃 성공&quot;));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가 좀 더 길어보이지만 그래도 &quot;일관성&quot;이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;회원가입 기능 강화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 테스트를 하면서 몇 가지 발견한 결함들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아이디 중복 검사 필요( 중복된 아이디가 저장되면 에러 발생함 )&lt;/li&gt;
&lt;li&gt;필수 입력인 필드 표시&lt;/li&gt;
&lt;li&gt;회원가입 버튼 &amp;amp; 엔터 누를 시 확인 창 하나 띄우기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단~! 제일 먼저 Member의 Entity를 확인해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1742117910119&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String userId;
    private String password;
    private String nickname;
    private String email;
    private String job;
    private int age;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 날 것의 Membe이다. 프론트 쪽에서는 간단하게나마 유효성 검사를 해놨기 때문에 프론트와 설정을 맞춰주고자 html을 열어봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;signup.html&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 정규식 같은 유효성 검사는 적혀있는데 글자수 이런 건 안적혀 있길래 또 추가해줬다.&lt;/p&gt;
&lt;pre id=&quot;code_1742121018856&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;signup-container&quot;&amp;gt;
    &amp;lt;h1&amp;gt;회원가입&amp;lt;/h1&amp;gt;
    &amp;lt;form id=&quot;signupForm&quot;&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;userId&quot;&amp;gt;아이디:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;userId&quot; name=&quot;userId&quot; placeholder=&quot;아이디 입력&quot;
                   pattern=&quot;^[A-Za-z][A-Za-z0-9]*$&quot;
                   minlength=&quot;2&quot; maxlength=&quot;30&quot;
                   title=&quot;영문+숫자. 숫자로 시작하면 안되며 2~30자여야 합니다.&quot; required/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;password&quot;&amp;gt;비밀번호:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;password&quot; id=&quot;password&quot; name=&quot;password&quot; placeholder=&quot;비밀번호 입력&quot;
                   pattern=&quot;^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&amp;amp;*()_+=-])[A-Za-z\\d!@#$%^&amp;amp;*()_+=-]{8,}$&quot;
                   minlength=&quot;8&quot; maxlength=&quot;30&quot;
                   title=&quot;비밀번호는 8~30자이며, 영어, 숫자, 특수문자(!@#$%^&amp;amp;*)가 모두 포함되어야 합니다.&quot;
                   required/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;confirmPassword&quot;&amp;gt;비밀번호 확인:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;password&quot; id=&quot;confirmPassword&quot; name=&quot;confirmPassword&quot; placeholder=&quot;비밀번호 확인&quot; required/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;nickname&quot;&amp;gt;닉네임:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;nickname&quot; name=&quot;nickname&quot; placeholder=&quot;닉네임은 2~20자로 입력해주세요.&quot;
                   minlength=&quot;2&quot; maxlength=&quot;20&quot; required/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;email&quot;&amp;gt;이메일:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;email&quot; id=&quot;email&quot; name=&quot;email&quot; placeholder=&quot;이메일 입력&quot; required/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;job&quot;&amp;gt;직업:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;job&quot; name=&quot;job&quot; placeholder=&quot;직업 입력&quot;/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;age&quot;&amp;gt;나이:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;number&quot; id=&quot;age&quot; name=&quot;age&quot; placeholder=&quot;나이 입력&quot;/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;button type=&quot;submit&quot;&amp;gt;회원가입&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/form&amp;gt;
    &amp;lt;!-- 결과 메시지를 보여줄 영역 --&amp;gt;
    &amp;lt;div id=&quot;message&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 기반으로 Member Entity에 유효성 어노테이션들을 추가하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SignupRequestDto.java&lt;/h3&gt;
&lt;pre id=&quot;code_1742125434029&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NotBlank
@Size(min = 2, max = 30)
@Pattern(regexp = &quot;^[A-Za-z][A-Za-z0-9]*$&quot;, message = &quot;아이디는 영문자로 시작하고, 영문자와 숫자만 포함해야합니다.&quot;)
private String userId;

@NotBlank
@Size(min = 8)
@Pattern(regexp = &quot;^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@!#$%^&amp;amp;*()_+=-])[A-Za-z\\d@!#$%^&amp;amp;*()_+=-]{8,}$\n&quot;
        , message = &quot;비밀번호는 8자 이상이며, 대문자 혹은 소문자, 숫자, 특수문자를 포함해야 합니다.&quot;)
private String password;

@NotBlank
@Size(min = 2, max = 20)
private String nickname;

@Email
private String email;

private String job;

@Min(0)
@Max(150)
private int age;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드에 @Pattern을 꼭 걸어야하나 싶어 ChatGPT한테 물어보니 서버쪽에도 당연히 유효성 정보를 세팅하는게 좋다고 한다. 클라이언트의 요청이 꼭 페이지만을 통해서 오지 않기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 포스트맨이나, cURL 등으로 API 요청을 보내면 서버쪽에서는 입력 값에 대해 검증할 방도가 없기 때문! 그래서 Pattern도 만들고 메세지도 적었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 어노테이션에 대한 검증들은 테스트 코드를 짜면서 해보겠다! 일단 스킵!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이제 회원가입 화면에 필수인 필드들을 사용자가 쉽게 확인할 수 있게 해보자.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;signup.html&lt;/h3&gt;
&lt;pre id=&quot;code_1742125978694&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- CSS --&amp;gt;
.required {
    color: red;
    font-weight: bold;
    margin-left: 5px;
}

&amp;lt;div&amp;gt;
    &amp;lt;label for=&quot;userId&quot;&amp;gt;&amp;lt;span class=&quot;required&quot;&amp;gt;*&amp;lt;/span&amp;gt; 아이디:&amp;lt;/label&amp;gt;
    &amp;lt;input type=&quot;text&quot; id=&quot;userId&quot; name=&quot;userId&quot; placeholder=&quot;아이디 입력&quot;
           pattern=&quot;^[A-Za-z][A-Za-z0-9]*$&quot;
           minlength=&quot;2&quot; maxlength=&quot;30&quot;
           title=&quot;영문+숫자. 숫자로 시작하면 안되며 2~30자여야 합니다.&quot; required/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 간단하게 아이디에만 추가해봤다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;452&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2xY8b/btsMLNNFqAu/AW20zX7fdR0A37MjMETON0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2xY8b/btsMLNNFqAu/AW20zX7fdR0A37MjMETON0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2xY8b/btsMLNNFqAu/AW20zX7fdR0A37MjMETON0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2xY8b%2FbtsMLNNFqAu%2FAW20zX7fdR0A37MjMETON0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;379&quot; height=&quot;186&quot; data-origin-width=&quot;452&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음 괜찮은 거 같아서 중복을 최대한 제거하기 위해 CSS 코드로 추가해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1742126364502&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- CSS --&amp;gt;
.required-label::before {
    content : &quot;* &quot;;
    color : red;
    font-weight : bold;
}
        
&amp;lt;!-- body --&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;label for=&quot;userId&quot; class=&quot;required-label&quot;&amp;gt; 아이디:&amp;lt;/label&amp;gt;
    &amp;lt;input type=&quot;text&quot; id=&quot;userId&quot; name=&quot;userId&quot; placeholder=&quot;아이디 입력&quot;
           pattern=&quot;^[A-Za-z][A-Za-z0-9]*$&quot;
           minlength=&quot;2&quot; maxlength=&quot;30&quot;
           title=&quot;영문+숫자. 숫자로 시작하면 안되며 2~30자여야 합니다.&quot; required/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 필수 컴포넌트의 label에 클래스를 추가해주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uuJyZ/btsMLJEBX8b/pdp4119xOvDmz9y81dWq50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uuJyZ/btsMLJEBX8b/pdp4119xOvDmz9y81dWq50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uuJyZ/btsMLJEBX8b/pdp4119xOvDmz9y81dWq50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuuJyZ%2FbtsMLJEBX8b%2Fpdp4119xOvDmz9y81dWq50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;358&quot; height=&quot;531&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 아이디 중복 체크 기능을 만들어보자. 화면 쪽에 중복 체크 버튼을 만들까하다가 네이버 회원가입에는 그냥 회원가입 버튼 누르면 중복 체크 결과를 알려주길래 나도 똑같이하려고 화면 쪽에 버튼을 추가하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 컨트롤러를 바꿔주자.&amp;nbsp; 이전 컨트롤러는 authService에 있는 signup() 메서드의 결과값을 받아와서 삼항연산자를 통해 응답이 ok일 경우와 error 일 경우 두 가지 방법으로 나눠서 보냈었는데 CustomException을 적용하면서 예외처리 하는 방식도 좀 바꿀거라 더 간단하게 만들어놨다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AuthController&lt;/h3&gt;
&lt;pre id=&quot;code_1742220424338&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;signup&quot;)
public ResponseEntity&amp;lt;ApiResponse&amp;lt;Boolean&amp;gt;&amp;gt; signup(@RequestBody SignupRequestDto signupRequestDto) {
    authService.signup(signupRequestDto);
    return ResponseEntity.ok(ApiResponse.success(true));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AuthService&lt;/h3&gt;
&lt;pre id=&quot;code_1742220497779&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 회원가입
 * @param signupRequestDto
 * @return
 */
@Transactional
public void signup(SignupRequestDto signupRequestDto) {
    String encodedPassword = passwordEncoder.encode(signupRequestDto.getPassword());
    checkUserIdDuplicate(signupRequestDto.getUserId());
    authRepository.save(signupRequestDto.toEntity(encodedPassword));
}

/**
 * 사용자 아이디 중복 체크
 * @param userId
 */
public void checkUserIdDuplicate(String userId) {
    boolean result = authRepository.existsByUserId(userId);
    if(result) {
        throw new DuplicateUserIdException(&quot;이미 사용 중인 아이디입니다.&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 아이디 중복 체크를 하는 checkUserIdDuplicate() 메서드를 만들고 중복일 경우 DuplicateUserIdException()을 던진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;DuplicateUserIdException&lt;/h3&gt;
&lt;pre id=&quot;code_1742220581690&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ResponseStatus(HttpStatus.CONFLICT)
public class DuplicateUserIdException extends RuntimeException{
    public DuplicateUserIdException(String message) {
        super(message);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 customException을 만들어준다. 그리고 전역 예외처리를 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GlobalExceptionHandler&lt;/h3&gt;
&lt;pre id=&quot;code_1742221310952&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(DuplicateUserIdException.class)
    public ResponseEntity&amp;lt;ApiResponse&amp;lt;String&amp;gt;&amp;gt; handelDuplicateUserIdException(DuplicateUserIdException ex) {
        return ResponseEntity.status(HttpStatus.CONFLICT)
                .body(ApiResponse.error(HttpStatus.CONFLICT, ex.getMessage()));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러의 경우도 Response&amp;lt;ApiResponse&amp;lt;?&amp;gt;&amp;gt;의 형식으로 응답이 반환되게 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 회원가입 화면 쪽에 async와 await이 적용안되어있길래 적용해주고 에러 message를 화면에 보여줄 수 있도록 전체적으로 바꿨다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;signup.html&lt;/h3&gt;
&lt;pre id=&quot;code_1742221464679&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    async function signup(event) {
        const password = document.getElementById('password').value;
        const password2 = document.getElementById('confirmPassword').value;
        if(password !== password2) {
            alert(&quot;비밀번호가 일치하지 않습니다.&quot;);
            return;
        }

        const formData = {
            userId: document.getElementById('userId').value
            , password: document.getElementById('password').value
            , nickname: document.getElementById('nickname').value
            , email: document.getElementById('email').value
            , job: document.getElementById('job').value
            , age: document.getElementById('age').value
        };

        try {
            // API 엔드포인트로  JSON 데이터 전송
            const response = await fetch('/api/auth/signup', {
                method: 'POST',
                headers: {
                    'Content-Type' : 'application/json'
                },
                body: JSON.stringify(formData)
            })

            const data = await response.json();
            console.log(&quot;서버 응답 데이터  : &quot; + data);

            if(!response.ok) {
                throw new Error(data.message);
            }

            // API 응답에 따라 메세지 출력
            alert(&quot;회원가입에 성공했습니다.&quot;);

            window.location.replace(&quot;login.html&quot;);
        } catch(error) {
            console.error('Error: ', error);
            document.getElementById('message').innerText = error.message;
        }
    }

    document.getElementById('signupForm').addEventListener('submit', function(event) {
        event.preventDefault(); // 기본 폼 제출 동작 차단
        signup(event);
    })
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 해보자! Member 테이블에는 이미 'test1'이라는 userId가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;143&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4Y0F3/btsMOfI2QtN/Qs73n0UvoPxPLAzApnkcLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4Y0F3/btsMOfI2QtN/Qs73n0UvoPxPLAzApnkcLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4Y0F3/btsMOfI2QtN/Qs73n0UvoPxPLAzApnkcLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4Y0F3%2FbtsMOfI2QtN%2FQs73n0UvoPxPLAzApnkcLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;512&quot; height=&quot;143&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;143&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에서 'test1'로 또 가입을 해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bILm0k/btsMNM1oXZe/bQGjdHHPTS3nxQzfXMDUTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bILm0k/btsMNM1oXZe/bQGjdHHPTS3nxQzfXMDUTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bILm0k/btsMNM1oXZe/bQGjdHHPTS3nxQzfXMDUTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbILm0k%2FbtsMNM1oXZe%2FbQGjdHHPTS3nxQzfXMDUTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;343&quot; height=&quot;500&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0MLgU/btsMM7kReKg/bU5ie3j705tktQNKQ83mak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0MLgU/btsMM7kReKg/bU5ie3j705tktQNKQ83mak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0MLgU/btsMM7kReKg/bU5ie3j705tktQNKQ83mak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0MLgU%2FbtsMM7kReKg%2FbU5ie3j705tktQNKQ83mak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1370&quot; height=&quot;763&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;763&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;409 에러가 발생하면서 서버에서 던진 error message가 화면에 출력된다. 참고로 응답 데이터는 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;74&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uwyYC/btsMN8pI84H/xrUAWsIvJwnhFtDRQKqnt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uwyYC/btsMN8pI84H/xrUAWsIvJwnhFtDRQKqnt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uwyYC/btsMN8pI84H/xrUAWsIvJwnhFtDRQKqnt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuwyYC%2FbtsMN8pI84H%2FxrUAWsIvJwnhFtDRQKqnt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;480&quot; height=&quot;74&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;74&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이디 중복 같은 경우는 따로 조금 더 리팩토링 해봐야겠다. Member 엔티티에 유효성 어노테이션이랑... customException 처리 쪽을 좀 손봐야할 듯.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 아이디 중복 검사 기능 완료!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 제일 간단한 회원가입 버튼을 눌렀을 때 confirm창을 띄워 사용자에게 한 번 더 확인받는 로직으로 변경하자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;signup.html&lt;/h3&gt;
&lt;pre id=&quot;code_1742222404799&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const result = window.confirm(&quot;회원가입을 진행하시겠습니까?&quot;);
if(!result) return;
signup(event);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;signup()를 호출하기 전에 confirm의 결과가 &quot;취소&quot;면 회원가입 api를 요청할 수 없게 해놨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘도 즐거운 코딩!!&lt;/p&gt;</description>
      <category>프로젝트 및 컨퍼런스 회고</category>
      <category>Java</category>
      <category>springboot</category>
      <category>웹</category>
      <category>토이프로젝트</category>
      <author>데부한</author>
      <guid isPermaLink="true">https://devhan.tistory.com/358</guid>
      <comments>https://devhan.tistory.com/358#entry358comment</comments>
      <pubDate>Mon, 17 Mar 2025 23:41:24 +0900</pubDate>
    </item>
    <item>
      <title>[토이 프로젝트] spring boot 스프링 시큐리티 토큰 적용하기 (Bearer Token)</title>
      <link>https://devhan.tistory.com/357</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이전 글&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741605772307&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[토이 프로젝트] spring boot 로그인 기능 만들기-2 (스프링 시큐리티 적용)&quot; data-og-description=&quot;이전 프로젝트&amp;nbsp;[토이 프로젝트] 회원가입 기능 만들기-1사실 아직 만들고 싶은 프로젝트는 생각안해봤다.다만 프로젝트를 해봐야 실력이 정말 늘 것 같아서 일단 대중적이고 레퍼런스 자료가 &quot; data-og-host=&quot;devhan.tistory.com&quot; data-og-source-url=&quot;https://devhan.tistory.com/356&quot; data-og-url=&quot;https://devhan.tistory.com/356&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c2KFiR/hyYmI8vZBt/iKDy0KzepNHkiUGbk0UKO0/img.png?width=800&amp;amp;height=622&amp;amp;face=0_0_800_622,https://scrap.kakaocdn.net/dn/uRfl2/hyYmJ7p8w2/nTTbdzpnKMBJu8xtcO1Pa1/img.png?width=800&amp;amp;height=622&amp;amp;face=0_0_800_622,https://scrap.kakaocdn.net/dn/0sSLd/hyYr2cNUkF/Hoa0PAcngGY8KQFrYsbTPK/img.png?width=995&amp;amp;height=774&amp;amp;face=0_0_995_774&quot;&gt;&lt;a href=&quot;https://devhan.tistory.com/356&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devhan.tistory.com/356&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c2KFiR/hyYmI8vZBt/iKDy0KzepNHkiUGbk0UKO0/img.png?width=800&amp;amp;height=622&amp;amp;face=0_0_800_622,https://scrap.kakaocdn.net/dn/uRfl2/hyYmJ7p8w2/nTTbdzpnKMBJu8xtcO1Pa1/img.png?width=800&amp;amp;height=622&amp;amp;face=0_0_800_622,https://scrap.kakaocdn.net/dn/0sSLd/hyYr2cNUkF/Hoa0PAcngGY8KQFrYsbTPK/img.png?width=995&amp;amp;height=774&amp;amp;face=0_0_995_774');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[토이 프로젝트] spring boot 로그인 기능 만들기-2 (스프링 시큐리티 적용)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이전 프로젝트&amp;nbsp;[토이 프로젝트] 회원가입 기능 만들기-1사실 아직 만들고 싶은 프로젝트는 생각안해봤다.다만 프로젝트를 해봐야 실력이 정말 늘 것 같아서 일단 대중적이고 레퍼런스 자료가&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devhan.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 게시글에서 스프링 시큐리티를 통과하는 단순한 로그인 기능을 구현했다. 문제점은 DB를 조회해서 유효성 검사하는 기능까지는 다 됐지만 로그인 이후에 인증이 필요한 페이지에 접속하면 접속이 안되는 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐면 스프링 시큐리티에 이 사용자는 인증된 사용자임을 모르기 때문이다. 즉, 인증 상태가 유지되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 http header에 Authorization 인증 정보를 추가해서 인증 유지를 시켜보려한다. http header에 인증 정보를 추가하는 방식에는 아래 세 가지 기술들이 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Bearer Token&lt;/li&gt;
&lt;li&gt;JWT&lt;/li&gt;
&lt;li&gt;OAuth 2.0&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 개 중에 제일 쉽고 간편한 1번&amp;nbsp; API Key 방식을 사용해서 토큰 인증 방식을 구현하겠다. 아, 그리고 비밀번호를 언제까지나 평문으로 저장할 수 없으니 비밀번호도 암호화해서 저장하는 방식을 추가로 또 적용해주자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;API Key&amp;nbsp;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구현 난이도가 쉬움&lt;/li&gt;
&lt;li&gt;서버에서 임의의 토큰을 생성후 클라이언트가 요청 시 헤더에 추가하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 먼저 해줄 작업은 PasswordEncoder를 AuthService에 의존성을 주입해준다. 그래서 SecurityConfig에서 PasswordEncode의 Bean을 추가해주고 AuthSerivce에서 불러오면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PasswordEncoder&lt;/h2&gt;
&lt;pre id=&quot;code_1741427623693&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
public class SecurityConfig {
	
	// ... 생략
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bean 등록을 해줬으니 이제 AuthService에서 주입해주면 된다. 그리고 token을 저장할 저장소도 같이 만들어준다.&lt;/p&gt;
&lt;pre id=&quot;code_1741427683765&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class AuthService {

    private final PasswordEncoder passwordEncoder;
    private final Map&amp;lt;String, String&amp;gt; tokenStore = new HashMap&amp;lt;&amp;gt;(); // 토큰 저장소
    //...
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 기존에 있던 AuthService에 있던 login()을 수정해주고 토큰을 발급하는 함수와 비밀번호를 체크하는 함수를 따로 만들어줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아, 그전에 LoginResponseDto에서 token 필드를 추가해준다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;로그인 시 토큰 발급&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LoginResponseDto&lt;/h3&gt;
&lt;pre id=&quot;code_1741430323996&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@AllArgsConstructor
@Builder
public class LoginResponseDto {

    private Long id;
    private String userId;
    private String nickname;
    private String email;
    private String job;
    private int age;
    private String token;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 AuthService를 수정한다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;AuthService&lt;/h3&gt;
&lt;pre id=&quot;code_1741430266586&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 로그인
 * @param loginRequestDto
 * @return
 */
public LoginResponseDto login(LoginRequestDto loginRequestDto) {
    Member findMember = authRepository.findByUserId(loginRequestDto.getUserId())
                    .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;회원 정보가 없습니다.&quot;));

    boolean passwordFlag = validatePassword(loginRequestDto.getPassword(), findMember.getPassword());

    String token = null;
    if(passwordFlag) {
        token = generateToken(findMember);
    } else {
        throw new IllegalArgumentException(&quot;비밀번호를 확인하세요.&quot;);
    }

    return LoginResponseDto.builder()
            .id(findMember.getId())
            .userId(findMember.getUserId())
            .email(findMember.getEmail())
            .job(findMember.getJob())
            .age(findMember.getAge())
            .nickname(findMember.getNickname())
            .token(token)
            .build();
}

/**
 * token 생성 후 반환
 * @param member
 * @return token
 */
public String generateToken(Member member) {
    String token = UUID.randomUUID().toString(); // 간단한 토큰 생성
    tokenStore.put(token, member.getUserId());
    return token;
}

/**
 * 비밀번호 검사
 * @param inputPassword
 * @param memberPassword
 * @return boolean
 */
public boolean validatePassword(String inputPassword, String memberPassword) {
    return passwordEncoder.matches(inputPassword, memberPassword);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로직은 간단하다.&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DB에 사용자 이름으로 member를 검색한다.&lt;/li&gt;
&lt;li&gt;사용자가 없으면 Exception을 던진다.&lt;/li&gt;
&lt;li&gt;사용자가 있으면 입력된 패스워드와 DB에 저장된 패스워드가 같은지 유효성 검사를 한다.&lt;/li&gt;
&lt;li&gt;패스워드가 맞으면 토큰을 UUID로 발급하고 틀리면 예외를 발생시킨다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 AuthController도 수정해준다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;AuthController&lt;/h3&gt;
&lt;pre id=&quot;code_1741430768605&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;login&quot;)
public ResponseEntity&amp;lt;?&amp;gt; login(@RequestBody LoginRequestDto loginRequestDto) {
    LoginResponseDto loginResponseDto = authService.login(loginRequestDto);
    if(loginResponseDto.getToken() == null) {
        ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(&quot;아이디와 비밀번호를 확인해주세요.&quot;);
    }
    return ResponseEntity.ok(loginResponseDto);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;token 값이 null이면 예외를 터트리고, 아니면 loginResponseDto를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 추가적인 작업! 비밀번호를 로그인 시 비밀번호 암호화 작업을해서 회원가입할 때도 이 부분을 추가해줘야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;회원가입 시 패스워드 암호화&lt;/h2&gt;
&lt;pre id=&quot;code_1741431968270&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public boolean signup(SignupRequestDto signupRequestDto) {
    String encodedPassword = passwordEncoder.encode(signupRequestDto.getPassword());
    authRepository.save(signupRequestDto.toEntity(encodedPassword));
    return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;signupRequestDto에서 toEntity() 메서드를 하나 더 만들어준다.&lt;/p&gt;
&lt;pre id=&quot;code_1741432005140&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Member toEntity(String encodedPassword) {
    return Member.builder()
            .userId(this.userId)
            .password(encodedPassword)
            .nickname(this.nickname)
            .email(this.email)
            .job(this.job)
            .age(this.age)
            .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 완성! 일단 회원가입 시 비밀번호가 제대로 암호화되는지 확인해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 일단 회원가입 페이지가 인증이 필요한 페이지라 제외해줬다. 그리고 테스트를 위해 인증이 필요한 화면 테스트용으로 하나 만들었다.&lt;/p&gt;
&lt;pre id=&quot;code_1741432227043&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf(csrf -&amp;gt; csrf.disable())
            .sessionManagement(session -&amp;gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests((authorize) -&amp;gt; authorize
                    .requestMatchers(&quot;/&quot;, &quot;/index.html&quot;, &quot;/member/*&quot;, &quot;/api/auth/*&quot;).permitAll()
                    .anyRequest().authenticated())
            .exceptionHandling(exceptions -&amp;gt; exceptions.authenticationEntryPoint(((request, response, authException)
                    -&amp;gt; response.sendError(HttpServletResponse.SC_UNAUTHORIZED)))); // 401 응답 반환
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cS6q8k/btsMECSRChK/7ZyEfDLCHcGwENg2vSryv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cS6q8k/btsMECSRChK/7ZyEfDLCHcGwENg2vSryv1/img.png&quot; data-alt=&quot;진짜 테스트용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cS6q8k/btsMECSRChK/7ZyEfDLCHcGwENg2vSryv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcS6q8k%2FbtsMECSRChK%2F7ZyEfDLCHcGwENg2vSryv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;259&quot; height=&quot;182&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;진짜 테스트용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 index.html에 테스트용 화면으로 이동할 수 있는 a 태그를 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1741432328892&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;h2&amp;gt;메인입니다.&amp;lt;/h2&amp;gt;
    &amp;lt;a href=&quot;member/login.html&quot;&amp;gt;로그인 이동&amp;lt;/a&amp;gt;
    &amp;lt;a href=&quot;member/signup.html&quot;&amp;gt;회원가입 이동&amp;lt;/a&amp;gt;
    &amp;lt;a href=&quot;test/test.html&quot;&amp;gt;인증 필요 화면&amp;lt;/a&amp;gt;
&amp;lt;/body&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러고 서버를 재시작하면 회원가입 페이지도 인증없이 접근할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 회원가입 후 DB를 조회해보면 평문이 아닌 암호문으로 저장된 패스워드를 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xE4QM/btsMFshnq9M/7K6MOvDvIuZ2ktrIG0cQh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xE4QM/btsMFshnq9M/7K6MOvDvIuZ2ktrIG0cQh1/img.png&quot; data-alt=&quot;$느좋$&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xE4QM/btsMFshnq9M/7K6MOvDvIuZ2ktrIG0cQh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxE4QM%2FbtsMFshnq9M%2F7K6MOvDvIuZ2ktrIG0cQh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;507&quot; height=&quot;158&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;$느좋$&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;인증이 필요한 request 요청 시 Authorization 헤더 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증이 필요한 request 요청 시 토큰을 검사하기 위해 토큰 관련 필터를 만들어준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TokenAuthenticationFilter&lt;/h3&gt;
&lt;pre id=&quot;code_1741433377432&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    private final AuthService authService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = getTokenFromRequest(request);

        if(token != null &amp;amp;&amp;amp; authService.validateToken(token)) {
            Authentication authentication 
                    = new UsernamePasswordAuthenticationToken(token, null, null);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        
        filterChain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader(&quot;Authorization&quot;);
        if(bearerToken != null &amp;amp;&amp;amp; bearerToken.startsWith(&quot;Bearer &quot;)) {
            return bearerToken.replace(&quot;Bearer &quot;, &quot;&quot;);
        }
        return null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;OncePerRequestFilter&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 시큐리티에서 제공하는 추상 클래스&lt;/li&gt;
&lt;li&gt;특정 요청이 들어올 때 &lt;b&gt;한 번&lt;/b&gt;만 필터링 로직을 수행하도록 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;getTokenFromRequest&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 http header에서 Authorization 속성을 가져옴&lt;/li&gt;
&lt;li&gt;Bearer로 시작하는 문자열이면서 null이 아니면 Bearer를 제거한 나머지 문자열을 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;doFilterInternal&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 http header에서 토큰 값을 가져옴&lt;/li&gt;
&lt;li&gt;토큰 값이 null이 아니면서 유효한 토큰이면 인증 정보를 스프링 시큐리티에 세팅&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Spring Security 필터 추가&lt;/h3&gt;
&lt;pre id=&quot;code_1741608278285&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// SecurityConfig

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, TokenAuthenticationFilter tokenAuthenticationFilter) throws Exception {
    http.csrf(csrf -&amp;gt; csrf.disable())
            .sessionManagement(session -&amp;gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests((authorize) -&amp;gt; authorize
                    .requestMatchers(&quot;/**&quot;, &quot;/&quot;, &quot;/index.html&quot;,&quot;/api/auth/*&quot;).permitAll()
                    .requestMatchers(&quot;/api/**&quot;).authenticated()
                    .anyRequest().authenticated())
            .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // 필터 추가
            .exceptionHandling(exceptions -&amp;gt; exceptions.authenticationEntryPoint(((request, response, authException)
                    -&amp;gt; response.sendError(HttpServletResponse.SC_UNAUTHORIZED)))); // 401 응답 반환
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;addFilterBefore()로 아까 만들었던 필터를 추가해주면 이제 API 요청마다 Authorization 헤더를 검사하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 거의 다 한 것 같지만 제일 중요한게 하나 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 프론트에서 로그인 api 요청의 응답 데이터를 받고 token 값을 세팅해줘야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;localStorage 세팅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;login.html&lt;/h3&gt;
&lt;pre id=&quot;code_1741616635868&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function login(event) {
    event.preventDefault();

    const formData = {
          userId : document.getElementById('userId').value
        , password : document.getElementById('password').value
    };

    try {
        const token = localStorage.getItem(&quot;token&quot;);

        const response = await fetch('/api/auth/login', {
            method : 'POST',
            headers : {
                'Content-Type' : 'application/json'
            },
            body: JSON.stringify(formData)
        });

        if(!response.ok) {
            throw new Error('아이디와 비밀번호를 확인하세요.');
        }

        const data = await response.json();
        localStorage.setItem(&quot;token&quot;, data.token); // token 값 추가
        alert(&quot;로그인 성공&quot;);

        window.location.href = &quot;../index.html&quot;;
    } catch(error) {
        console.error('Error :', error);
        alert(&quot;로그인 실패: &quot; + error.message);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 인증이 필요한 페이지에서 /api/auth/validate api를 사용해 token의 유효 여부를 체크한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;test.html&lt;/h3&gt;
&lt;pre id=&quot;code_1741616821896&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;Title&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h2&amp;gt;인증이 필요한 페이지&amp;lt;/h2&amp;gt;
&amp;lt;script&amp;gt;

    window.onload = async function() {
        const token = localStorage.getItem(&quot;token&quot;);
        if(!token) {
            redirectToLogin();
            return;
        }

        try {
            const response = await fetch(&quot;/api/auth/validate&quot;, {
                method : &quot;GET&quot;,
                headers: {
                    &quot;Authorization&quot;:&quot;Bearer &quot; + token
                }
            });

            if(!response.ok) {
                throw new Error(&quot;유효하지 않은 토큰&quot;);
            }
        }catch(error) {
            redirectToLogin();
        }
    };

    function redirectToLogin() {
        alert(&quot;로그인이 필요합니다.&quot;);
        localStorage.removeItem(&quot;token&quot;); // 유효하지 않은 토큰 삭제
        window.location.href = &quot;/member/login.html&quot;;
    }
&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 로그인이 필요한 페이지에 token 유효성 검사 후 token이 유효하면 접근 가능하게하고 유효하지 않으면 login 페이지로 리다이렉트 시켜버리면 Bearer 토큰을 이용한 방식은 끝이 났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아 마지막으로 테스트 해야지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 없이 일단 인증이 필요한 페이지로 접속해보자. Local Storage는 당연히 비어있다. 이 화면에서 alert의 확인 버튼을 누르면 로그인 페이지로 리다이렉트 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1229&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sT8wq/btsMHyuLDfI/iMikJ7lUMXesXW9VyzpJZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sT8wq/btsMHyuLDfI/iMikJ7lUMXesXW9VyzpJZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sT8wq/btsMHyuLDfI/iMikJ7lUMXesXW9VyzpJZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsT8wq%2FbtsMHyuLDfI%2FiMikJ7lUMXesXW9VyzpJZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;796&quot; height=&quot;223&quot; data-origin-width=&quot;1229&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뒤로가기 후 회원가입하고 로그인을 해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1231&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwU1tY/btsMEZnzqB8/KKWXHUz7bafT5FYAJ689k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwU1tY/btsMEZnzqB8/KKWXHUz7bafT5FYAJ689k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwU1tY/btsMEZnzqB8/KKWXHUz7bafT5FYAJ689k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwU1tY%2FbtsMEZnzqB8%2FKKWXHUz7bafT5FYAJ689k0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;733&quot; height=&quot;388&quot; data-origin-width=&quot;1231&quot; data-origin-height=&quot;652&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인이 성공하면 localStorage에 token이 저장된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1222&quot; data-origin-height=&quot;319&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rPIiT/btsMFxw91fq/i217kGjNzva4qe2yy1iA01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rPIiT/btsMFxw91fq/i217kGjNzva4qe2yy1iA01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rPIiT/btsMFxw91fq/i217kGjNzva4qe2yy1iA01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrPIiT%2FbtsMFxw91fq%2Fi217kGjNzva4qe2yy1iA01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;680&quot; height=&quot;178&quot; data-origin-width=&quot;1222&quot; data-origin-height=&quot;319&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 인증 필요 화면으로 이동!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1221&quot; data-origin-height=&quot;321&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coCajz/btsMHpSd305/UN15TxaP81Hfu0XEbd2sJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coCajz/btsMHpSd305/UN15TxaP81Hfu0XEbd2sJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coCajz/btsMHpSd305/UN15TxaP81Hfu0XEbd2sJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoCajz%2FbtsMHpSd305%2FUN15TxaP81Hfu0XEbd2sJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;792&quot; height=&quot;208&quot; data-origin-width=&quot;1221&quot; data-origin-height=&quot;321&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 화면으로 리다이렉트 되지 않고 정상적으로 페이지가 로드된것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bearer 토큰은 이렇게 프론트에서 토큰을 localStorage 또는 sessionStorage에 저장 후 API 요청 시마다 Authorization 헤더에 포함시키는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 공통화가 필수이다. api 인증을 보낼 때마다 각 화면 스크립트에서 /api/auth/validate api를 호출하는 소스를 작성하면 유지보수도 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;js 파일을 따로 빼서 공통화를 추천한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 임시로 거쳐갈 테스트 뿐이라 공통화는 하지 않았다. Bearer 토큰 인증 방식에 대한 간단한 구현과 이해만 하고 이제 다음에는 JWT 토큰을 사용한 방식을 구현해볼까한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니면 로그인 기능을 조금 더 강화할 수도 있겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 오늘도 즐거운 코딩..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트 및 컨퍼런스 회고/프로젝트</category>
      <category>Java</category>
      <category>springboot</category>
      <category>토이프로젝트</category>
      <author>데부한</author>
      <guid isPermaLink="true">https://devhan.tistory.com/357</guid>
      <comments>https://devhan.tistory.com/357#entry357comment</comments>
      <pubDate>Mon, 10 Mar 2025 23:40:01 +0900</pubDate>
    </item>
    <item>
      <title>[토이 프로젝트] spring boot 로그인 기능 만들기-2 (스프링 시큐리티 적용)</title>
      <link>https://devhan.tistory.com/356</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이전 프로젝트&lt;/p&gt;
&lt;figure id=&quot;og_1740845602711&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[토이 프로젝트] 회원가입 기능 만들기-1&quot; data-og-description=&quot;사실 아직 만들고 싶은 프로젝트는 생각안해봤다.다만 프로젝트를 해봐야 실력이 정말 늘 것 같아서 일단 대중적이고 레퍼런스 자료가 제일 많은커뮤니티 사이트를 만들어보려한다.&amp;nbsp;환경은 일&quot; data-og-host=&quot;devhan.tistory.com&quot; data-og-source-url=&quot;https://devhan.tistory.com/354&quot; data-og-url=&quot;https://devhan.tistory.com/354&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cBBw5l/hyYjpOFT8n/kOvgBfull8gTrZLSZ0yCV1/img.png?width=800&amp;amp;height=613&amp;amp;face=0_0_800_613,https://scrap.kakaocdn.net/dn/G3WAa/hyYjks2m82/ooi5zF2vXubUeWGrhPsQl1/img.png?width=800&amp;amp;height=613&amp;amp;face=0_0_800_613,https://scrap.kakaocdn.net/dn/bLBA4s/hyYjqzjIxX/LVkB0dP3kfELPOHzdqsKSk/img.png?width=1245&amp;amp;height=954&amp;amp;face=0_0_1245_954&quot;&gt;&lt;a href=&quot;https://devhan.tistory.com/354&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devhan.tistory.com/354&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cBBw5l/hyYjpOFT8n/kOvgBfull8gTrZLSZ0yCV1/img.png?width=800&amp;amp;height=613&amp;amp;face=0_0_800_613,https://scrap.kakaocdn.net/dn/G3WAa/hyYjks2m82/ooi5zF2vXubUeWGrhPsQl1/img.png?width=800&amp;amp;height=613&amp;amp;face=0_0_800_613,https://scrap.kakaocdn.net/dn/bLBA4s/hyYjqzjIxX/LVkB0dP3kfELPOHzdqsKSk/img.png?width=1245&amp;amp;height=954&amp;amp;face=0_0_1245_954');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[토이 프로젝트] 회원가입 기능 만들기-1&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;사실 아직 만들고 싶은 프로젝트는 생각안해봤다.다만 프로젝트를 해봐야 실력이 정말 늘 것 같아서 일단 대중적이고 레퍼런스 자료가 제일 많은커뮤니티 사이트를 만들어보려한다.&amp;nbsp;환경은 일&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devhan.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 로그인 기능에 스프링 시큐리티를 적용해서 보안을 강화해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, build.gradle에 스프링 시큐리티를 추가해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1740845696014&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
	implementation &quot;org.springframework.boot:spring-boot-starter-security&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 기능(ex.OAuth 2.0 등)을 동시에 적용할 수 있겠지만 일단 차근차근 적용해보는 거로 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 일단 제일 기본적인 아이디/비밀번호 로그인 기반 spring security 기능을 적용해서 인증 기능을 구현하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 공식 페이지에서 가이드를 확인했다.&lt;/p&gt;
&lt;figure id=&quot;og_1740848579084&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Username/Password Authentication :: Spring Security&quot; data-og-description=&quot;Normally, Spring Security builds an AuthenticationManager internally composed of a DaoAuthenticationProvider for username/password authentication. In certain cases, it may still be desired to customize the instance of AuthenticationManager used by Spring S&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/index.html&quot; data-og-url=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/index.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/index.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Username/Password Authentication :: Spring Security&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Normally, Spring Security builds an AuthenticationManager internally composed of a DaoAuthenticationProvider for username/password authentication. In certain cases, it may still be desired to customize the instance of AuthenticationManager used by Spring S&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d1JA9a/btsMBdRHude/WNWlx5IE4el2kMXdIAni41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d1JA9a/btsMBdRHude/WNWlx5IE4el2kMXdIAni41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d1JA9a/btsMBdRHude/WNWlx5IE4el2kMXdIAni41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd1JA9a%2FbtsMBdRHude%2FWNWlx5IE4el2kMXdIAni41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;995&quot; height=&quot;774&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 이렇게 알려준 코드를 config 패키지를 만들어 똑같이 파일을 만들어주고 소스를 붙여넣었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 코드에 대한 자세한 설명이 없고, 구글 번역기를 사용해서 문장이 불완전했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 챗GPT한테 물어보고 이것 저것 건드려본 결과를 정리하면..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어노테이션&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Configuration : 설정 클래스임을 스프링 부트에게 알려줌.&lt;/li&gt;
&lt;li&gt;EnableWebSecurity : spring securtiy 활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;securityFilterChain()&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티에서 사용하는 SecurityFilterChain을 반환. securityFilterChain()이 없으면 기본적으로 위 코드의 내용으로 디폴트 세팅이 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SecurityFilterChain() 호출 흐름&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Spring Boot를 실행하면 @EnableWebSecurity 활성화&lt;/li&gt;
&lt;li&gt;Spring security가 @Bean을 가진 클래스 스캔&lt;/li&gt;
&lt;li&gt;securityFilterChain()을 찾아 자동으로 실행하여 보안 설정 적용&lt;/li&gt;
&lt;li&gt;SecurityFilterChain이 Spring Security의 FilterChainProxy에 등록됨&lt;/li&gt;
&lt;li&gt;요청이 들어올 때 FilterChainProxy가 이를 가로채 보안 검사 수행&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 스프링 시큐리티가 자동으로 securityFilterChain()을 호출하니 보안 규칙을 정해 해당 메서드 안에 기재하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;authorizeHttpRequests&lt;/h3&gt;
&lt;pre id=&quot;code_1740849952084&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http.authorizeHttpRequests((authorize) -&amp;gt; authorize
        .anyRequest().authenticated()) // 모든 요청에 대해 인증 필요&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 모든 요청에 대해 인증을 요구하는 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 로그인하지 않으면 그 어떠한 페이지에도 접근할 수가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 위해 해당 코드만 남기고 나머지는 주석처리하고 서버를 돌려봤다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbMLbT/btsMAHeDXHh/4mikfdm2gWBQaDZNkXH4s0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbMLbT/btsMAHeDXHh/4mikfdm2gWBQaDZNkXH4s0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbMLbT/btsMAHeDXHh/4mikfdm2gWBQaDZNkXH4s0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbMLbT%2FbtsMAHeDXHh%2F4mikfdm2gWBQaDZNkXH4s0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;716&quot; height=&quot;169&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;207&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 실행하고 localhost:8080으로 접속하면 권한이 없다는 페이지가 보인다. HTTP error 코드는 403.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;549&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNWt4H/btsMy4BR79p/oWJNQqkUzpvjiCzK12ktN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNWt4H/btsMy4BR79p/oWJNQqkUzpvjiCzK12ktN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNWt4H/btsMy4BR79p/oWJNQqkUzpvjiCzK12ktN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNWt4H%2FbtsMy4BR79p%2FoWJNQqkUzpvjiCzK12ktN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;564&quot; height=&quot;361&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;549&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 메인 화면은 누구나 접근하게 하고 싶으면 어떻게할까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 permitAll()을 사용하면 된다. 그래서 바로 permitAll()을 추가하고 서버를 돌려봤다.&lt;/p&gt;
&lt;pre id=&quot;code_1740850646211&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests((authorize) -&amp;gt; authorize
            .requestMatchers(&quot;/&quot;).permitAll() // 메인 화면은 로그인 없이 접근 가능
            .anyRequest().authenticated()); // 모든 요청에 대해 인증 필요 -&amp;gt; 로그인하지 않으면 어떠한 페이지도 접근 X
//                .httpBasic(Customizer.withDefaults()) // HTTP Basic 인증 활성화
//               .formLogin(Customizer.withDefaults()); // 폼 로그인 활성화
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;527&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qE4t7/btsMzYuq9N9/HpKLutCGLlat6KKksQ6Sl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qE4t7/btsMzYuq9N9/HpKLutCGLlat6KKksQ6Sl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qE4t7/btsMzYuq9N9/HpKLutCGLlat6KKksQ6Sl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqE4t7%2FbtsMzYuq9N9%2FHpKLutCGLlat6KKksQ6Sl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;575&quot; height=&quot;381&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;527&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 여전히 403 에러가 발생했다. 뭐가 문젠지하고 찾아보다가 permitAll()로 로그인 없이 접근 가능한 경로를 설정해도 스프링 시큐리티에서 로그인 기능 formLogin(), httpBasic()이 없으면 아예 403 에러를 터트려버린다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 임시로 formLogin()까지 설정해주고 서버를 돌려봤다.&lt;/p&gt;
&lt;pre id=&quot;code_1740850794596&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests((authorize) -&amp;gt; authorize
            .requestMatchers(&quot;/&quot;).permitAll() // 메인 화면은 로그인 없이 접근 가능
            .anyRequest().authenticated()) // 모든 요청에 대해 인증 필요 -&amp;gt; 로그인하지 않으면 어떠한 페이지도 접근 X
//                .httpBasic(Customizer.withDefaults()) // HTTP Basic 인증 활성화
            .formLogin(Customizer.withDefaults()); // 폼 로그인 활성화
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;597&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSmG3y/btsMzrKlbTh/6t6Lnk7XYautcQegOMvbfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSmG3y/btsMzrKlbTh/6t6Lnk7XYautcQegOMvbfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSmG3y/btsMzrKlbTh/6t6Lnk7XYautcQegOMvbfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSmG3y%2FbtsMzrKlbTh%2F6t6Lnk7XYautcQegOMvbfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;523&quot; height=&quot;289&quot; data-origin-width=&quot;597&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;localhost:8080으로 접속했는데 기대와는 달리 /login 페이지로 리다이렉트 된다. 이것저것 다양하게 경로를 추가해봤는데 결론적으로는 &quot;/index.html&quot;도 추가해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1740852128521&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests((authorize) -&amp;gt; authorize
            .requestMatchers(&quot;/&quot;, &quot;/index.html&quot;).permitAll()
            .anyRequest().authenticated())
//                .httpBasic(Customizer.withDefaults()) // HTTP Basic 인증 활성화
            .formLogin(Customizer.withDefaults()); // 폼 로그인 활성화
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;/&quot;만 추가했을때 안되는 이유는 spring boot에서는 정적 리소스를 자동으로 서빙해준다. /static/index.html 파일을 만들면 브라우저에서 &quot;localhost:8080/&quot;만 입력해도 index.html 화면을 띄워준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 spring security 입장에서는 &quot;/&quot; != &quot;/index.html&quot;이다. 제일 처음 GET 요청인 &quot;/&quot; 경로는 permitAll()로 통과시킨다. 그래서 spring boot가 &quot;/index.html&quot;을 서빙하려고 하니까 spring security 입장에서는 &quot;/&quot;가 아니기 때문에 /login 경로로 리다이렉트 시켜버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 재시작하고 localhost:8080으로 접속하면 이제 메인이 잘 뜬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;352&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brdAPB/btsMzgbaMRr/5Gi0vMQ4zyakl8RTKSh9F0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brdAPB/btsMzgbaMRr/5Gi0vMQ4zyakl8RTKSh9F0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brdAPB/btsMzgbaMRr/5Gi0vMQ4zyakl8RTKSh9F0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrdAPB%2FbtsMzgbaMRr%2F5Gi0vMQ4zyakl8RTKSh9F0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;352&quot; height=&quot;228&quot; data-origin-width=&quot;352&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;formLogin()&lt;/h3&gt;
&lt;pre id=&quot;code_1740853343915&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests((authorize) -&amp;gt; authorize
            .requestMatchers(&quot;/&quot;, &quot;/index.html&quot;).permitAll()
            .anyRequest().authenticated())
//                .httpBasic(Customizer.withDefaults()) // HTTP Basic 인증 활성화
            .formLogin(Customizer.withDefaults()); // 폼 로그인 활성화
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 .formLogin()은 어떤 역할을 하는 소스일까. 말 그대로 폼 로그인 기능을 활성화 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매개변수로 Customizer.withDefaults()를 입력하면 기본적으로 제공하는 로그인 페이지로 리다이렉트 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 위에서 /login으로 리다이렉트 됐을 때 초면인 화면이 보여졌던 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckxKn2/btsMAJpVULJ/9Q8NdSeYqkTi6Ha6WAcgp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckxKn2/btsMAJpVULJ/9Q8NdSeYqkTi6Ha6WAcgp1/img.png&quot; data-origin-width=&quot;382&quot; data-origin-height=&quot;253&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;46.12&quot; style=&quot;width: 45.5843%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckxKn2/btsMAJpVULJ/9Q8NdSeYqkTi6Ha6WAcgp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckxKn2%2FbtsMAJpVULJ%2F9Q8NdSeYqkTi6Ha6WAcgp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;382&quot; height=&quot;253&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MxmwG/btsMx94WYTS/YQmZFlPK6czn0mD6rxd0OK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MxmwG/btsMx94WYTS/YQmZFlPK6czn0mD6rxd0OK/img.png&quot; data-origin-width=&quot;508&quot; data-origin-height=&quot;288&quot; data-is-animation=&quot;false&quot; style=&quot;width: 53.2529%;&quot; data-widthpercent=&quot;53.88&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MxmwG/btsMx94WYTS/YQmZFlPK6czn0mD6rxd0OK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMxmwG%2FbtsMx94WYTS%2FYQmZFlPK6czn0mD6rxd0OK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;508&quot; height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;좌측 : 스프링 시큐리티 기본 제공 로그인 화면 / 우측 : 내가 만들었던 로그인 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 만들었던 로그인 화면을 &quot;/login&quot;으로 접속했을 때 띄우려면 loginPage() 설정과 requesetMatchers() 설정을 해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1740925895136&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests((authorize) -&amp;gt; authorize
            .requestMatchers(&quot;/&quot;, &quot;/index.html&quot;, &quot;/member/login.html&quot;).permitAll() // 수정
            .anyRequest().authenticated())
//                .httpBasic(Customizer.withDefaults()) // HTTP Basic 인증 활성화
            .formLogin(form -&amp;gt; form.loginPage(&quot;/member/login.html&quot;)); // 수정
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 후 서버를 재시작해보면 짠~&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1970&quot; data-origin-height=&quot;1072&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQNESX/btsMAWphkoL/kRrLljLwpMOYEnarPV25u1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQNESX/btsMAWphkoL/kRrLljLwpMOYEnarPV25u1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQNESX/btsMAWphkoL/kRrLljLwpMOYEnarPV25u1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQNESX%2FbtsMAWphkoL%2FkRrLljLwpMOYEnarPV25u1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;627&quot; height=&quot;341&quot; data-origin-width=&quot;1970&quot; data-origin-height=&quot;1072&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 만들었던 로그인 화면이 잘 나온다. 근데 사실 RESTful API로 개발할 때는 .formLogin()과 .httpBasic()이 필요하지가 않다고 한다. 그래서 필요없는 저 메서드 두 개를 사용하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스를 전반적으로 RESTful API에 맞춰보면 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1741002221746&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf(csrf -&amp;gt; csrf.disable())
            .sessionManagement(session -&amp;gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests((authorize) -&amp;gt; authorize
                    .requestMatchers(&quot;/&quot;, &quot;/index.html&quot;, &quot;/member/login.html&quot;, &quot;/api/auth/login&quot;).permitAll()
                    .anyRequest().authenticated())
            .exceptionHandling(exceptions -&amp;gt; exceptions.authenticationEntryPoint(((request, response, authException)
                    -&amp;gt; response.sendError(HttpServletResponse.SC_UNAUTHORIZED)))); // 401 응답 반환
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 관련 설정은 꼭 STATELESS로 설정해줘야 한다. 현재로써는 세션 관련 기능이 없기 때문에 세션 기능을 사용 안함으로 설정해주지 않으면 스프링 시큐리티는 세션 기반이 디폴트라 계속 세션을 체크해서 어떤짓을 해도 401 에러를 뱉을 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 원활한 테스트를 위해 메인 화면에 a 태그로 login 화면을 이동할 수 있도록 하나 만들어준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n08Ya/btsMBkKkFGy/kGPAYwFF5pPk3Elope0jDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n08Ya/btsMBkKkFGy/kGPAYwFF5pPk3Elope0jDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n08Ya/btsMBkKkFGy/kGPAYwFF5pPk3Elope0jDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn08Ya%2FbtsMBkKkFGy%2FkGPAYwFF5pPk3Elope0jDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;320&quot; height=&quot;170&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;332&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 이동 글씨를 클릭하면 해당 화면으로 잘 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 DB를 확인하고 member 테이블에 데이터가 없으면 insert문으로 데이터 넣어주고 로그인을 진행해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1784&quot; data-origin-height=&quot;1368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biAz17/btsMzrD1u8y/tdgP9nESvvjdkf0lAWUUR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biAz17/btsMzrD1u8y/tdgP9nESvvjdkf0lAWUUR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biAz17/btsMzrD1u8y/tdgP9nESvvjdkf0lAWUUR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiAz17%2FbtsMzrD1u8y%2FtdgP9nESvvjdkf0lAWUUR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;592&quot; height=&quot;454&quot; data-origin-width=&quot;1784&quot; data-origin-height=&quot;1368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 실패가 떨어진다. 개발자 도구로 네트워크 요청 메세지를 확인해보면 401 에러 코드를 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3lcmf/btsMBFHugDz/ITfcvC3mHNfd0LOnLbH8oK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3lcmf/btsMBFHugDz/ITfcvC3mHNfd0LOnLbH8oK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3lcmf/btsMBFHugDz/ITfcvC3mHNfd0LOnLbH8oK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3lcmf%2FbtsMBFHugDz%2FITfcvC3mHNfd0LOnLbH8oK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;613&quot; height=&quot;238&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 그러는지 생각해보다가 /api/auth/login 요청 주소도 스프링 시큐리티의 거미줄에 걸리지 않게 제외해줘야겠다는 생각이 들었다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1741003030501&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.requestMatchers(&quot;/&quot;, &quot;/index.html&quot;, &quot;/member/login.html&quot;, &quot;/api/auth/login&quot;).permitAll()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 서버 재실행 후 테스트해보면! 로그인이 성공하는 걸 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qZ48a/btsMzO0bfgG/kn8icgnVz50P7yUR1LvWJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qZ48a/btsMzO0bfgG/kn8icgnVz50P7yUR1LvWJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qZ48a/btsMzO0bfgG/kn8icgnVz50P7yUR1LvWJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqZ48a%2FbtsMzO0bfgG%2Fkn8icgnVz50P7yUR1LvWJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;631&quot; height=&quot;395&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 문제는 로그인 후에.. 현재 인증된 회원만 접근할 수 있는 '회원가입' 페이지에 접근할 수가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 당연하다. 진짜 단순한 로그인 기능을 구현했기 때문에 구현 소스 안에 스프링 시큐리티가 인증된 사용자 정보를 유지하지 못하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 스프링 시큐리티는 로그인 후에 인증 정보를 세션에 저장하고 이 세션을 기반으로 사용자의 인증 여부를 체크한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 위에서 세션 기능을 꺼버려서 이 상태에서는 인증 정보 유지가 안 된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 세션 기능을 끌거면 매 api 요청마다 인증된 사용자임을 표현(?)해줘야하는데 대부분 http header에 Authorization을 추가해서 인증 정보를 전달하는 방식으로 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 다음에.. 알아보자!&lt;/p&gt;</description>
      <category>프로젝트 및 컨퍼런스 회고/프로젝트</category>
      <category>springboot</category>
      <category>springsecurity</category>
      <category>토이프로젝트</category>
      <author>데부한</author>
      <guid isPermaLink="true">https://devhan.tistory.com/356</guid>
      <comments>https://devhan.tistory.com/356#entry356comment</comments>
      <pubDate>Mon, 3 Mar 2025 22:41:52 +0900</pubDate>
    </item>
    <item>
      <title>[토이 프로젝트] spring boot 로그인 기능 만들기-1</title>
      <link>https://devhan.tistory.com/355</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이전 게시글&lt;/p&gt;
&lt;figure id=&quot;og_1740573056852&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[토이 프로젝트] 회원가입 기능 만들기-1&quot; data-og-description=&quot;사실 아직 만들고 싶은 프로젝트는 생각안해봤다.다만 프로젝트를 해봐야 실력이 정말 늘 것 같아서 일단 대중적이고 레퍼런스 자료가 제일 많은커뮤니티 사이트를 만들어보려한다.&amp;nbsp;환경은 일&quot; data-og-host=&quot;devhan.tistory.com&quot; data-og-source-url=&quot;https://devhan.tistory.com/354&quot; data-og-url=&quot;https://devhan.tistory.com/354&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/d6gtPG/hyYjyRRTaG/ApT5bkRSKS0hxwS5LcbJkK/img.png?width=800&amp;amp;height=613&amp;amp;face=0_0_800_613,https://scrap.kakaocdn.net/dn/zwHBP/hyYjklP8Jr/tY3sSFm7wVkT2kHLFvC6Gk/img.png?width=800&amp;amp;height=613&amp;amp;face=0_0_800_613,https://scrap.kakaocdn.net/dn/pu8jQ/hyYmPqV5bm/8K1C5xhAUkW3vfaVOdPUK1/img.png?width=1245&amp;amp;height=954&amp;amp;face=0_0_1245_954&quot;&gt;&lt;a href=&quot;https://devhan.tistory.com/354&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devhan.tistory.com/354&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/d6gtPG/hyYjyRRTaG/ApT5bkRSKS0hxwS5LcbJkK/img.png?width=800&amp;amp;height=613&amp;amp;face=0_0_800_613,https://scrap.kakaocdn.net/dn/zwHBP/hyYjklP8Jr/tY3sSFm7wVkT2kHLFvC6Gk/img.png?width=800&amp;amp;height=613&amp;amp;face=0_0_800_613,https://scrap.kakaocdn.net/dn/pu8jQ/hyYmPqV5bm/8K1C5xhAUkW3vfaVOdPUK1/img.png?width=1245&amp;amp;height=954&amp;amp;face=0_0_1245_954');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[토이 프로젝트] 회원가입 기능 만들기-1&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;사실 아직 만들고 싶은 프로젝트는 생각안해봤다.다만 프로젝트를 해봐야 실력이 정말 늘 것 같아서 일단 대중적이고 레퍼런스 자료가 제일 많은커뮤니티 사이트를 만들어보려한다.&amp;nbsp;환경은 일&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devhan.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 처음으로 회원가입 기능을 만들었다. 회원가입 기능을 고도화할까 하다가 로그인 기능을 먼저 만들고 하는 게 나을 거 같아서 오늘은 로그인 기능을 만들어보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 로그인 화면을 만들어주자. CSS는 일단 회원가입 화면에서 썼던 거 가져왔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSaBN4/btsMwfYtW2i/SfGrXNtkpGh3764CZ9Byfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSaBN4/btsMwfYtW2i/SfGrXNtkpGh3764CZ9Byfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSaBN4/btsMwfYtW2i/SfGrXNtkpGh3764CZ9Byfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSaBN4%2FbtsMwfYtW2i%2FSfGrXNtkpGh3764CZ9Byfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;514&quot; height=&quot;296&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1740579383248&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;body&amp;gt;
&amp;lt;div class=&quot;signup-container&quot;&amp;gt;
    &amp;lt;form id=&quot;loginForm&quot;&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;userId&quot;&amp;gt;아이디:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;userId&quot; name=&quot;userId&quot; placeholder=&quot;아이디 입력&quot;
                   pattern=&quot;^[A-Za-z][A-Za-z0-9]{1,}$&quot;
                   title=&quot;영문+숫자. 숫자로 시작하면 안되며 최소 두 글자 이상이어야 합니다.&quot; required/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;password&quot;&amp;gt;비밀번호:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;password&quot; id=&quot;password&quot; name=&quot;password&quot; placeholder=&quot;비밀번호 입력&quot; required/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;button type=&quot;submit&quot;&amp;gt;로그인&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 스크립트를 작성해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 기능은 사용자의 정보를 전송하기 때문에 GET으로 요청하기보다는 POST로 요청하는 게 맞다.&lt;/p&gt;
&lt;pre id=&quot;code_1740581521043&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    async function login(event) {
        event.preventDefault();

        const formData = {
              userId : document.getElementById('userId').value
            , password : document.getElementById('password').value
        };

        try {
            const response = await fetch('/api/auth/login', {
                method : 'POST',
                headers : {
                    'Content-Type' : 'application/json'
                },
                body: JSON.stringify(formData)
            });

            if(!response.ok) {
                throw new Error('아이디와 비밀번호를 확인하세요.');
            }

            const data = await response.json();

            alert(&quot;로그인 성공&quot;);

            window.location.href = &quot;../index.html&quot;;
        } catch(error) {
            console.error('Error :', error);
            alert(&quot;로그인 실패: &quot; + error.message);
        }
    }

    document.addEventListener('DOMContentLoaded', function() {
        document.getElementById('loginForm').addEventListener('submit', login);
    });

&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;POST method를 사용하는 이유&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;비밀번호와 같은 민감한 데이터를 Body에 담아 전송&lt;/li&gt;
&lt;li&gt;GET은 브라우저나 프록시 서버에 캐싱될 수 있지만, POST는 캐싱되지 않음&lt;/li&gt;
&lt;li&gt;GET을 사용하면 url 뒤로 쿼리스트링이 붙음&lt;/li&gt;
&lt;li&gt;로그인을 하면 백엔드 쪽에서는 JWT 토큰을 발급하는 등의 상태 변화가 일어남으로 POST가 더 적절&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 백엔드 소스를 만들자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 기능 만들때 별 생각없이 패키지를 member로 했는데 auth로 변경하고 파일 이름도 다 변경해줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthController에 login()추가&lt;/p&gt;
&lt;pre id=&quot;code_1740639892507&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;login&quot;)
public ResponseEntity&amp;lt;LoginResponseDto&amp;gt; login(@RequestBody LoginRequestDto loginRequestDto) {
    LoginResponseDto loginResponseDto = authService.login(loginRequestDto);
    return ResponseEntity.ok(loginResponseDto);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoginRequestDto, LoginResponseDto 추가&lt;/p&gt;
&lt;pre id=&quot;code_1740639932641&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
public class LoginRequestDto {
    private String userId;
    private String password;
}


// ==============================

@Getter
@AllArgsConstructor
@Builder
public class LoginResponseDto {

    private Long id;
    private String userId;
    private String nickname;
    private String email;
    private String job;
    private int age;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthService에 login() 추가&lt;/p&gt;
&lt;pre id=&quot;code_1740639962619&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 로그인
 * @param loginRequestDto
 * @return
 */
public LoginResponseDto login(LoginRequestDto loginRequestDto) {
    Member findMember = authRepository.findByUserIdAndPassword(loginRequestDto.getUserId()
            , loginRequestDto.getPassword()).orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;아이디와 비밀번호를 다시 확인하세요.&quot;));

    return LoginResponseDto.builder()
            .id(findMember.getId())
            .userId(findMember.getUserId())
            .email(findMember.getEmail())
            .job(findMember.getJob())
            .age(findMember.getAge())
            .nickname(findMember.getNickname())
            .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthRepository에 findByUserIdAndPassword() 추가&lt;/p&gt;
&lt;pre id=&quot;code_1740640001821&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface AuthRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {
    Optional&amp;lt;Member&amp;gt; findByUserIdAndPassword(String id, String password);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이제 서버 돌려서 테스트!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 접속&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6heAL/btsMzsn3xV0/2tbqKKqagbFx2maeAKH6N1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6heAL/btsMzsn3xV0/2tbqKKqagbFx2maeAKH6N1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6heAL/btsMzsn3xV0/2tbqKKqagbFx2maeAKH6N1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6heAL%2FbtsMzsn3xV0%2F2tbqKKqagbFx2maeAKH6N1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;426&quot; height=&quot;182&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 이동&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;501&quot; data-origin-height=&quot;763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eRBKrs/btsMx2qrKUu/HkGMhpmKLjwS5TbjyR4Grk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eRBKrs/btsMx2qrKUu/HkGMhpmKLjwS5TbjyR4Grk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eRBKrs/btsMx2qrKUu/HkGMhpmKLjwS5TbjyR4Grk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeRBKrs%2FbtsMx2qrKUu%2FHkGMhpmKLjwS5TbjyR4Grk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;416&quot; height=&quot;634&quot; data-origin-width=&quot;501&quot; data-origin-height=&quot;763&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용 채우고 회원가입 버튼 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;459&quot; data-origin-height=&quot;159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FMqfl/btsMx8c0gMm/g5fpBCpi5Fnx39Gyd3DwjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FMqfl/btsMx8c0gMm/g5fpBCpi5Fnx39Gyd3DwjK/img.png&quot; data-alt=&quot;성공!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FMqfl/btsMx8c0gMm/g5fpBCpi5Fnx39Gyd3DwjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFMqfl%2FbtsMx8c0gMm%2Fg5fpBCpi5Fnx39Gyd3DwjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;459&quot; height=&quot;159&quot; data-origin-width=&quot;459&quot; data-origin-height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;성공!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공하면 로그인 화면으로 이동한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;502&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nnjWF/btsMxDj8gNX/hxqaDkgWFVkyhYrlmGjYI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nnjWF/btsMxDj8gNX/hxqaDkgWFVkyhYrlmGjYI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nnjWF/btsMxDj8gNX/hxqaDkgWFVkyhYrlmGjYI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnnjWF%2FbtsMxDj8gNX%2FhxqaDkgWFVkyhYrlmGjYI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;502&quot; height=&quot;290&quot; data-origin-width=&quot;502&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이디, 비밀번호 틀리게 입력해보기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFVQLF/btsMzhNH1fV/YM7P8LrAPmUhCTeypAggA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFVQLF/btsMzhNH1fV/YM7P8LrAPmUhCTeypAggA0/img.png&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;283&quot; data-is-animation=&quot;false&quot; style=&quot;width: 37.1061%; margin-right: 10px;&quot; data-widthpercent=&quot;37.54&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFVQLF/btsMzhNH1fV/YM7P8LrAPmUhCTeypAggA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFVQLF%2FbtsMzhNH1fV%2FYM7P8LrAPmUhCTeypAggA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;490&quot; height=&quot;283&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bclRFz/btsMw6Aix1n/ikbP9KTKpX3l4coYhfrM70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bclRFz/btsMw6Aix1n/ikbP9KTKpX3l4coYhfrM70/img.png&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;159&quot; data-is-animation=&quot;false&quot; style=&quot;width: 61.7311%;&quot; data-widthpercent=&quot;62.46&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bclRFz/btsMw6Aix1n/ikbP9KTKpX3l4coYhfrM70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbclRFz%2FbtsMw6Aix1n%2FikbP9KTKpX3l4coYhfrM70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;458&quot; height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;실패!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올바른 아이디, 비밀번호 입력하기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F0YBg/btsMw2LxlvO/4q5E6yic1p2UToi895G1f1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F0YBg/btsMw2LxlvO/4q5E6yic1p2UToi895G1f1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F0YBg/btsMw2LxlvO/4q5E6yic1p2UToi895G1f1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF0YBg%2FbtsMw2LxlvO%2F4q5E6yic1p2UToi895G1f1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;820&quot; height=&quot;734&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 성공 시 메인 페이지로 이동한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PW5wa/btsMxCes05u/07AO0w5j9dv8CbRXrf2rJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PW5wa/btsMxCes05u/07AO0w5j9dv8CbRXrf2rJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PW5wa/btsMxCes05u/07AO0w5j9dv8CbRXrf2rJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPW5wa%2FbtsMxCes05u%2F07AO0w5j9dv8CbRXrf2rJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;372&quot; height=&quot;128&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 로그인 기능 완벽 적용!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토대를 만들었으니 메인에 로그인 버튼도 만들고....&lt;br /&gt;로그인하면 회원가입도 없애는 등... 작업도 해주고 스프링 시큐리티 도입이나 암호화 적용을 이제 해야겠다.&lt;/p&gt;</description>
      <category>프로젝트 및 컨퍼런스 회고/프로젝트</category>
      <category>login</category>
      <category>springboot</category>
      <category>토이프로젝트</category>
      <author>데부한</author>
      <guid isPermaLink="true">https://devhan.tistory.com/355</guid>
      <comments>https://devhan.tistory.com/355#entry355comment</comments>
      <pubDate>Thu, 27 Feb 2025 16:13:15 +0900</pubDate>
    </item>
    <item>
      <title>[토이 프로젝트] spring boot 회원가입 기능 만들기-1</title>
      <link>https://devhan.tistory.com/354</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 아직 만들고 싶은 프로젝트는 생각안해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 프로젝트를 해봐야 실력이 정말 늘 것 같아서 일단 대중적이고 레퍼런스 자료가 제일 많은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커뮤니티 사이트를 만들어보려한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경은 일단 간단하게만 생각해봤다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;spring boot 3.4.2&lt;/li&gt;
&lt;li&gt;JPA&lt;/li&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;li&gt;Thymeleaf -&amp;gt; react(욕심)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트를 예~전에 한 번 쓰윽 봤었으나 전혀 기억이 안나서 백지랑 똑같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 빨리 끝내는게 목표니 서버쪽을 Thymeleaf로 후다닥 만들고 리액트와 Next.js를 좀 배워서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면쪽을 바꿔 볼 생각이다. 뭐 생각은 일단 그렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼.. 얼른 시작!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프로젝트는 아래와 같이 만들었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1245&quot; data-origin-height=&quot;954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wtx0P/btsMrvSEQLb/cEWgerIoyKIomJqRLeCxS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wtx0P/btsMrvSEQLb/cEWgerIoyKIomJqRLeCxS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wtx0P/btsMrvSEQLb/cEWgerIoyKIomJqRLeCxS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwtx0P%2FbtsMrvSEQLb%2FcEWgerIoyKIomJqRLeCxS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;803&quot; height=&quot;615&quot; data-origin-width=&quot;1245&quot; data-origin-height=&quot;954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 여러 번 만들어봤지만 제일 떨리는 순간은 Generate 버튼을 누를 때!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃 레포도 하나 만들어줬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/noVRm/btsMpn214yA/j9g0rU5oxk4DYrJTgtK2s0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/noVRm/btsMpn214yA/j9g0rU5oxk4DYrJTgtK2s0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/noVRm/btsMpn214yA/j9g0rU5oxk4DYrJTgtK2s0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnoVRm%2FbtsMpn214yA%2Fj9g0rU5oxk4DYrJTgtK2s0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;726&quot; height=&quot;327&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클론 후 git ignore 설정을 위해 아래 사이트의 도움을 받는다.&lt;/p&gt;
&lt;figure id=&quot;og_1740054500578&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;gitignore.io&quot; data-og-description=&quot;Create useful .gitignore files for your project&quot; data-og-host=&quot;www.toptal.com&quot; data-og-source-url=&quot;https://www.toptal.com/developers/gitignore&quot; data-og-url=&quot;https://www.toptal.com/developers/gitignore&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/0kfSr/hyYjGHRvwy/VZjPAO2Hulf8jJaQ8xYbH1/img.png?width=2400&amp;amp;height=1254&amp;amp;face=0_0_2400_1254&quot;&gt;&lt;a href=&quot;https://www.toptal.com/developers/gitignore&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.toptal.com/developers/gitignore&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/0kfSr/hyYjGHRvwy/VZjPAO2Hulf8jJaQ8xYbH1/img.png?width=2400&amp;amp;height=1254&amp;amp;face=0_0_2400_1254');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;gitignore.io&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Create useful .gitignore files for your project&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.toptal.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.gitignore 파일에 내용을 붙여넣고 커밋한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 프로젝트 파일의 압축을 풀고 또 깃에 올린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브랜치까지 따줬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFcE3v/btsMqKCVbn9/OzY8GS1DX3BB2qmCLQL7k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFcE3v/btsMqKCVbn9/OzY8GS1DX3BB2qmCLQL7k1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFcE3v/btsMqKCVbn9/OzY8GS1DX3BB2qmCLQL7k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFcE3v%2FbtsMqKCVbn9%2FOzY8GS1DX3BB2qmCLQL7k1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;179&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;179&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mysql DB까지 만들어주고&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;166&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QLXPN/btsMtRJNIUW/VnzsqN52VK2BJa0Jl6dW2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QLXPN/btsMtRJNIUW/VnzsqN52VK2BJa0Jl6dW2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QLXPN/btsMtRJNIUW/VnzsqN52VK2BJa0Jl6dW2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQLXPN%2FbtsMtRJNIUW%2FVnzsqN52VK2BJa0Jl6dW2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;166&quot; height=&quot;152&quot; data-origin-width=&quot;166&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml 설정도 해줬다.&lt;/p&gt;
&lt;pre id=&quot;code_1740398018728&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  application:
    name: justComm

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/justComm?useSSL=false&amp;amp;serverTimezone=Asia/Seoul&amp;amp;characterEncoding=UTF-8
    username: root
    password:
  jpa:
    open-in-view: true
    hibernate:
      ddl-auto: create
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    show-sql: true
    properties:
      hibernate.format_sql: true
      dialect: org.hibernate.dialect.MySQL8InnoDBDialect


logging:
  level:
    org.hibernate.sql : debug&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러고 메인 화면을 만드려고했는데 잘 생각해보니 Next.js 전환 계획을 갖고 있기 때문에 RESTful api로 개발할 거라 타임리프보다는 그냥 HTML + javascript 조합이 더 좋을 거 같아 타임리프를 뒤로 미뤄두고 HTML로 열심히 꾸미기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 메인의 경로는..!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;169&quot; data-origin-height=&quot;70&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdRhbp/btsMv3V4IOW/DCaKikQMfWoj6KarG0lsn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdRhbp/btsMv3V4IOW/DCaKikQMfWoj6KarG0lsn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdRhbp/btsMv3V4IOW/DCaKikQMfWoj6KarG0lsn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdRhbp%2FbtsMv3V4IOW%2FDCaKikQMfWoj6KarG0lsn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;169&quot; height=&quot;70&quot; data-origin-width=&quot;169&quot; data-origin-height=&quot;70&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;resources/static 경로에 index.html 파일을 만들어주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1740398348907&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;메인&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h2&amp;gt;메인입니다.&amp;lt;/h2&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 서버 실행 후 localhost:8080으로 접속하면 메인이 딱 뜬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;333&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpTSZf/btsMwewvdvP/3300w3kLWZJPPGttzxcVx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpTSZf/btsMwewvdvP/3300w3kLWZJPPGttzxcVx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpTSZf/btsMwewvdvP/3300w3kLWZJPPGttzxcVx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpTSZf%2FbtsMwewvdvP%2F3300w3kLWZJPPGttzxcVx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;333&quot; height=&quot;138&quot; data-origin-width=&quot;333&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이제 회원가입 화면을 만들어보자. resources/static/member/signup.html로 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;183&quot; data-origin-height=&quot;119&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2HFBE/btsMv3V4RKO/vyPigf8xJC4x35p4KyIctK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2HFBE/btsMv3V4RKO/vyPigf8xJC4x35p4KyIctK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2HFBE/btsMv3V4RKO/vyPigf8xJC4x35p4KyIctK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2HFBE%2FbtsMv3V4RKO%2FvyPigf8xJC4x35p4KyIctK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;183&quot; height=&quot;119&quot; data-origin-width=&quot;183&quot; data-origin-height=&quot;119&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 index.html에 a 링크를 하나 만들어 회원가입 화면으로 이동할 수 있게 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1740398627093&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;a href=&quot;member/signup.html&quot;&amp;gt;회원가입 이동&amp;lt;/a&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 빠른 개발 속도를 위해 챗GPT한테 일정 틀을 주고 CSS 꾸며달라고했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;757&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nvGA0/btsMuu1B1DU/w7n2yg8m5ekUVAksKK1nt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nvGA0/btsMuu1B1DU/w7n2yg8m5ekUVAksKK1nt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nvGA0/btsMuu1B1DU/w7n2yg8m5ekUVAksKK1nt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnvGA0%2FbtsMuu1B1DU%2Fw7n2yg8m5ekUVAksKK1nt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;494&quot; height=&quot;757&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;757&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나름 예쁘게 꾸며줌 ㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 자바스크립트를 작성하면 된다. 너무 오랜만이라 기억이 안나 챗GPT의 도움을 좀 받았다.&lt;/p&gt;
&lt;pre id=&quot;code_1740401406093&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;body&amp;gt;
&amp;lt;div class=&quot;signup-container&quot;&amp;gt;
    &amp;lt;h1&amp;gt;회원가입&amp;lt;/h1&amp;gt;
    &amp;lt;form id=&quot;signupForm&quot;&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;userId&quot;&amp;gt;아이디:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;userId&quot; name=&quot;userId&quot; placeholder=&quot;아이디 입력&quot;
                   pattern=&quot;^[A-Za-z][A-Za-z0-9]{1,}$&quot;
                   title=&quot;영문+숫자. 숫자로 시작하면 안되며 최소 두 글자 이상이어야 합니다.&quot; required/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;password&quot;&amp;gt;비밀번호:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;password&quot; id=&quot;password&quot; name=&quot;password&quot; placeholder=&quot;비밀번호 입력&quot;
                   pattern=&quot;^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&amp;amp;*])[A-Za-z\d!@#$%^&amp;amp;*]{8,}$&quot;
                   title=&quot;비밀번호는 최소 8자 이상이며, 영어, 숫자, 특수문자(!@#$%^&amp;amp;*)가 모두 포함되어야 합니다.&quot;
                   required/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;confirmPassword&quot;&amp;gt;비밀번호 확인:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;password&quot; id=&quot;confirmPassword&quot; name=&quot;confirmPassword&quot; placeholder=&quot;비밀번호 확인&quot; required/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;nickname&quot;&amp;gt;닉네임:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;nickname&quot; name=&quot;nickname&quot; placeholder=&quot;닉네임 입력&quot; minlength=&quot;2&quot; required/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;email&quot;&amp;gt;이메일:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;email&quot; id=&quot;email&quot; name=&quot;email&quot; placeholder=&quot;이메일 입력&quot; required/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;job&quot;&amp;gt;직업:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;job&quot; name=&quot;job&quot; placeholder=&quot;직업 입력&quot;/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;age&quot;&amp;gt;나이:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;number&quot; id=&quot;age&quot; name=&quot;age&quot; placeholder=&quot;나이 입력&quot;/&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;button type=&quot;submit&quot;&amp;gt;회원가입&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/form&amp;gt;
    &amp;lt;!-- 결과 메시지를 보여줄 영역 --&amp;gt;
    &amp;lt;div id=&quot;message&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
    document.getElementById('signupForm').addEventListener('submit', function(event) {
        event.preventDefault(); // 기본 폼 제출 동작 차단

        const password = document.getElementById('password').value;
        const password2 = document.getElementById('confirmPassword').value;
        if(password !== password2) {
            alert(&quot;비밀번호가 일치하지 않습니다.&quot;);
            return;
        }

        const formData = {
              userId: document.getElementById('userId').value
            , password: document.getElementById('password').value
            , nickname: document.getElementById('nickname').value
            , email: document.getElementById('email').value
            , job: document.getElementById('job').value
            , age: document.getElementById('age').value
        };

        // API 엔드포인트로  JSON 데이터 전송
        fetch('/api/member', {
            method: 'POST',
            headers: {
                'Content-Type' : 'application/json'
            },
            body: JSON.stringify(formData)
        })
            .then(response =&amp;gt; response.json())
            .then(data =&amp;gt; {
                // API 응답에 따라 메세지 출력
                alert(&quot;회원가입에 성공했습니다.&quot;);
                setTimeout(() =&amp;gt; {
                    window.location.href = &quot;login.html&quot;;
                }, 1000);
            })
            .catch(error =&amp;gt; {
                console.error('Error: ', error);
                document.getElementById('message').innerText = '회원가입 중 오류 발생';
            });
    })

&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;submit 이벤트를 가로채서 기본 폼 제출 동작을 차단하고 비밀번호와 비밀번호 확인을 검증 후 통과하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폼에 작성된 모든 데이터를 JSON 객체로 만들어 api로&amp;nbsp; 쏴버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 응답이 잘 오면 회원가입 완료라는 alert 창과 함께 login 페이지로 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;login.html 페이지도 만들어서 body에 h2 태그 안에 로그인 화면이라고만 적어놨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이제 서버쪽 기능을 만들어야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Member 엔티티를 만들어준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;195&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dCZFUM/btsMtQ5kZpq/PEM9pxrlFHU2xqniNASem0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dCZFUM/btsMtQ5kZpq/PEM9pxrlFHU2xqniNASem0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dCZFUM/btsMtQ5kZpq/PEM9pxrlFHU2xqniNASem0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdCZFUM%2FbtsMtQ5kZpq%2FPEM9pxrlFHU2xqniNASem0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;195&quot; height=&quot;125&quot; data-origin-width=&quot;195&quot; data-origin-height=&quot;125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1740402386455&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String userId;
    private String password;
    private String nickname;
    private String email;
    private String job;
    private int age;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 간단하게 저렇게만 만들어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 MemberRequestDto도 만들어보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;198&quot; data-origin-height=&quot;151&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XKwpK/btsMwdRWigY/QazfAKKKutvjnYkWaHzrAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XKwpK/btsMwdRWigY/QazfAKKKutvjnYkWaHzrAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XKwpK/btsMwdRWigY/QazfAKKKutvjnYkWaHzrAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXKwpK%2FbtsMwdRWigY%2FQazfAKKKutvjnYkWaHzrAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;198&quot; height=&quot;151&quot; data-origin-width=&quot;198&quot; data-origin-height=&quot;151&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1740402644818&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
public class MemberRequest {

    private String userId;
    private String password;
    private String nickname;
    private String email;
    private String job;
    private int age;


    public Member toEntity() {
        return Member.builder()
                .userId(this.userId)
                .password(this.password)
                .nickname(this.nickname)
                .email(this.email)
                .job(this.job)
                .age(this.age)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Controller를 만들어보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;206&quot; data-origin-height=&quot;153&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lp8nD/btsMuywmwj5/asqoototnCW9pWo1VyOxPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lp8nD/btsMuywmwj5/asqoototnCW9pWo1VyOxPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lp8nD/btsMuywmwj5/asqoototnCW9pWo1VyOxPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flp8nD%2FbtsMuywmwj5%2FasqoototnCW9pWo1VyOxPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;206&quot; height=&quot;153&quot; data-origin-width=&quot;206&quot; data-origin-height=&quot;153&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1740402703497&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/member&quot;)
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    @PostMapping()
    public boolean signup(@RequestBody MemberRequest memberRequest) {

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;휴 Service랑 repository를 먼저 만들고 오자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;224&quot; data-origin-height=&quot;219&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XKhZM/btsMvgu4V1Y/Z0Czibol2XB9dSxfXp2Jv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XKhZM/btsMvgu4V1Y/Z0Czibol2XB9dSxfXp2Jv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XKhZM/btsMvgu4V1Y/Z0Czibol2XB9dSxfXp2Jv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXKhZM%2FbtsMvgu4V1Y%2FZ0Czibol2XB9dSxfXp2Jv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;224&quot; height=&quot;219&quot; data-origin-width=&quot;224&quot; data-origin-height=&quot;219&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1740402896912&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;


}


//================================

public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 다시 Controller 작성&lt;/p&gt;
&lt;pre id=&quot;code_1740404753310&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping()
public ResponseEntity signup(@RequestBody MemberRequest memberRequest) {
    boolean result = memberService.signup(memberRequest);
    return ResponseEntity.ok(result);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service의 signup 메서드도 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1740404784468&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 회원가입
 * @param memberRequest
 * @return
 */
@Transactional
public boolean signup(MemberRequest memberRequest) {
    memberRepository.save(memberRequest.toEntity());
    return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가 매우 부자연스러운 로직이지만 일단 이렇게하고 넘어가자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에.. 시간이 되면 고쳐! 얼른 하고 자야된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 이제 테스트를 해볼시간!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QSwqe/btsMuyC7vtK/K9A50koXJkeysFVtlX8Hak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QSwqe/btsMuyC7vtK/K9A50koXJkeysFVtlX8Hak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QSwqe/btsMuyC7vtK/K9A50koXJkeysFVtlX8Hak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQSwqe%2FbtsMuyC7vtK%2FK9A50koXJkeysFVtlX8Hak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;494&quot; height=&quot;750&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 입력하다가 엔터를 눌러서;; 갑자기 회원가입이 진행되어버렸다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;573&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2EOZH/btsMuywnuBY/qsAXgEsA3K8ASemFQlKq60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2EOZH/btsMuywnuBY/qsAXgEsA3K8ASemFQlKq60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2EOZH/btsMuywnuBY/qsAXgEsA3K8ASemFQlKq60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2EOZH%2FbtsMuywnuBY%2FqsAXgEsA3K8ASemFQlKq60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;573&quot; height=&quot;214&quot; data-origin-width=&quot;573&quot; data-origin-height=&quot;214&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장..된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서도 .. 확인&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1614&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjyywg/btsMwA7c4OD/YjgcWTWORSICTtK2Cmp4GK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjyywg/btsMwA7c4OD/YjgcWTWORSICTtK2Cmp4GK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjyywg/btsMwA7c4OD/YjgcWTWORSICTtK2Cmp4GK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjyywg%2FbtsMwA7c4OD%2FYjgcWTWORSICTtK2Cmp4GK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1614&quot; height=&quot;276&quot; data-origin-width=&quot;1614&quot; data-origin-height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;20살이 되고 싶었던 나는 0살이 되어버렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 회원가입 기능을 다듬어보는 시간을 가져야겠다.&lt;/p&gt;</description>
      <category>프로젝트 및 컨퍼런스 회고/프로젝트</category>
      <category>Java</category>
      <category>JPA</category>
      <category>springboot</category>
      <category>웹개발</category>
      <category>토이프로젝트</category>
      <author>데부한</author>
      <guid isPermaLink="true">https://devhan.tistory.com/354</guid>
      <comments>https://devhan.tistory.com/354#entry354comment</comments>
      <pubDate>Mon, 24 Feb 2025 22:49:55 +0900</pubDate>
    </item>
    <item>
      <title>넥사크로에서 지원하지 않는 CSS 적용하기</title>
      <link>https://devhan.tistory.com/353</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;넥사크로에서는 지원하지 않는 CSS를 테마 파일에 넣으면 테마 파일을 저장할 때 아예 에러가 나버린다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;352&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JbuPU/btsMqaBEJrm/Jt6W97liVItnOugVXtFBY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JbuPU/btsMqaBEJrm/Jt6W97liVItnOugVXtFBY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JbuPU/btsMqaBEJrm/Jt6W97liVItnOugVXtFBY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJbuPU%2FbtsMqaBEJrm%2FJt6W97liVItnOugVXtFBY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;352&quot; height=&quot;104&quot; data-origin-width=&quot;352&quot; data-origin-height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 어떻게 하면 사용할 수 있지 하다가 자바스크립트니까 스크립트에서 객체에 접근해서 설정할 수도 있을 거란 생각이 들어서 바로 테스트해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 화면에 돌려버릴 버튼 하나를 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;186&quot; data-origin-height=&quot;211&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8ylzN/btsMqIEHv01/Ft7OhVi0T0DcuIcLmrxNm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8ylzN/btsMqIEHv01/Ft7OhVi0T0DcuIcLmrxNm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8ylzN/btsMqIEHv01/Ft7OhVi0T0DcuIcLmrxNm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8ylzN%2FbtsMqIEHv01%2FFt7OhVi0T0DcuIcLmrxNm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;186&quot; height=&quot;211&quot; data-origin-width=&quot;186&quot; data-origin-height=&quot;211&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 버튼에 onmouseenter(), onmouseleave() 이벤트를 걸어주고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폼에도 onload() 이벤트를 걸어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 스크립트에서 document로 객체를 불러와 style을 직접 준다.&lt;/p&gt;
&lt;pre id=&quot;code_1740035400727&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;this.fvButton;

this.Form_Work_onload = function(obj:nexacro.Form,e:nexacro.LoadEventInfo)
{
	this.fvButton = document.getElementById(this.Button00._unique_id);
	this.fvButton.style.setProperty(&quot;transition&quot;, &quot;transform 0.3s ease&quot;);
};


this.Button00_onmouseenter = function(obj:nexacro.Button,e:nexacro.MouseEventInfo)
{
	this.fvButton.style.setProperty(&quot;transform&quot;, &quot;rotate(10deg)&quot;);
};

this.Button00_onmouseleave = function(obj:nexacro.Button,e:nexacro.MouseEventInfo)
{
	this.fvButton.style.setProperty(&quot;transform&quot;, &quot;rotate(0deg)&quot;);
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게하면 잘 동작하는 걸 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 이런 특별한 CSS를 넣을 수 있는 화면이 많다면.. 차라리 그냥 안하는게 더 좋을 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif-7ae41dd0a0a35e.gif&quot; data-origin-width=&quot;262&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QOknm/btsMoHU4lOk/uWTl0IWE4bCyG6pfFJtJw0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QOknm/btsMoHU4lOk/uWTl0IWE4bCyG6pfFJtJw0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QOknm/btsMoHU4lOk/uWTl0IWE4bCyG6pfFJtJw0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/QOknm/btsMoHU4lOk/uWTl0IWE4bCyG6pfFJtJw0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;262&quot; height=&quot;290&quot; data-filename=&quot;ezgif-7ae41dd0a0a35e.gif&quot; data-origin-width=&quot;262&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 무조건 안된다는 말보다는 일단 생각해보고 테스트 해보는게 중요한 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제해결능력을 더 쑥쑥 키우자.&lt;/p&gt;</description>
      <category>공부/넥사크로</category>
      <category>CSS</category>
      <category>넥사크로</category>
      <author>데부한</author>
      <guid isPermaLink="true">https://devhan.tistory.com/353</guid>
      <comments>https://devhan.tistory.com/353#entry353comment</comments>
      <pubDate>Thu, 20 Feb 2025 16:13:52 +0900</pubDate>
    </item>
    <item>
      <title>프로그래머스_문자열 나누기 JAVA</title>
      <link>https://devhan.tistory.com/352</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckiId7/btsMheyezmf/3KURbzyPqbhnUn4Y79oIE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckiId7/btsMheyezmf/3KURbzyPqbhnUn4Y79oIE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckiId7/btsMheyezmf/3KURbzyPqbhnUn4Y79oIE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckiId7%2FbtsMheyezmf%2F3KURbzyPqbhnUn4Y79oIE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;259&quot; height=&quot;405&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;405&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;figure id=&quot;og_1739328924708&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/140108&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/77dgN/hyYcfeb8QV/FDm2sDbnNCwGopLBMSJ1S1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/140108&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/140108&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/77dgN/hyYcfeb8QV/FDm2sDbnNCwGopLBMSJ1S1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제는 2주차 마지막 문제.. '문자열 나누기'다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 마지막 문젠데.. 생각보다 쉬워서 놀라버리기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;챗GPT.... 커리큘럼 잘 못짜는구나?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어찌됐든 푼다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드&lt;/h2&gt;
&lt;pre id=&quot;code_1739452791825&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {
    public int solution(String s) {
        int answer = 0;
        int same = 1;
        int notSame = 0;
        char targetChar = s.charAt(0);
        
        if(s.length() == 1) return 1;
        
        for(int i = 1; i &amp;lt; s.length(); i++) {
            char curChar = s.charAt(i);
            if(targetChar == curChar) same++;
            else notSame++;
            
            if(i+1 == s.length()) return answer+1;
            if(same == notSame) {
                answer++;
                
                targetChar = s.charAt(i+1);
                same = 0;
                notSame = 0;
            }
        }
        
        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이번 문제는 약속 시간이 한시간 좀 넘게 붕 떠서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;밖에서 급하게 작성한거라,, 정말 지문에 있는 그대로를 코드로 옮겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉬운문제라 따로 설명은 안하겠다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘도 즐거운 코딩!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그나저나 내일 아주 짧은 프로젝트,,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불 끄러 간 프로젝트 철수날이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혼자 거의 오픈이 가까운 프로젝트에 투입돼서 공통적인 결함들을 수정하는거라 좀,,,,,, 쫄? 했는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어려워 보이는 문제들은 투입 전에 미리 생각해놓은 덕분에 거의 200%를 고치고 와서 다행이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니,,? 300% 일지도&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대-견&lt;/p&gt;</description>
      <category>알고리즘/알고리즘 풀이</category>
      <category>Java</category>
      <category>코딩테스트</category>
      <category>프로그래머스</category>
      <author>데부한</author>
      <guid isPermaLink="true">https://devhan.tistory.com/352</guid>
      <comments>https://devhan.tistory.com/352#entry352comment</comments>
      <pubDate>Thu, 13 Feb 2025 22:22:54 +0900</pubDate>
    </item>
    <item>
      <title>프로그래머스_최빈값 구하기 JAVA</title>
      <link>https://devhan.tistory.com/351</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sc4k1/btsMjhGQWn1/oWSgMGWK1JBvjNcDzQ77w0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sc4k1/btsMjhGQWn1/oWSgMGWK1JBvjNcDzQ77w0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sc4k1/btsMjhGQWn1/oWSgMGWK1JBvjNcDzQ77w0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsc4k1%2FbtsMjhGQWn1%2FoWSgMGWK1JBvjNcDzQ77w0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;259&quot; height=&quot;405&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;405&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;figure id=&quot;og_1738898142004&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/120812&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gAwDt/hyYch9Pvy1/Y6xoSha4aY3vOkcZaaCq8K/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/120812&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/120812&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gAwDt/hyYch9Pvy1/Y6xoSha4aY3vOkcZaaCq8K/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 풀 문제는 '최빈값'이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 푸는 문제라 과거 코드는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제는 짧고 요구사항이 정확해서 마음에 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드&lt;/h2&gt;
&lt;pre id=&quot;code_1738898159077&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public int solution(int[] array) {
        int answer = -1;
        Map&amp;lt;Integer, Integer&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
        
        for(int i = 0; i &amp;lt; array.length; i++) {
            map.put(array[i], map.getOrDefault(array[i], 0)+1);
        }
        
        int max = Collections.max(map.values());
        for(Map.Entry&amp;lt;Integer, Integer&amp;gt; entry : map.entrySet()) {
            Integer key = entry.getKey();
            Integer value = entry.getValue();
            
            if(value == max) {
                if(answer &amp;gt; -1) return -1;
                answer = key;
            }
        }
        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자료구조는 Map을 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;map에 데이터를 넣을 때 중복 체크를하려고 한 순간&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영한쓰의 자바 강의에서 getOrDefault()를 사용했던게 떠올라 이 메서드를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가 모를 흡족함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돈을 허투루 쓴게 아니구만!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제도 사무실 보안때문에 캡처본이 안올라가;;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점수를 캡처 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점수는 +3점을 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘도 즐거운 코딩!!!&lt;/p&gt;</description>
      <category>알고리즘/알고리즘 풀이</category>
      <category>Java</category>
      <category>코딩테스트</category>
      <category>프로그래머스</category>
      <author>데부한</author>
      <guid isPermaLink="true">https://devhan.tistory.com/351</guid>
      <comments>https://devhan.tistory.com/351#entry351comment</comments>
      <pubDate>Thu, 13 Feb 2025 22:18:21 +0900</pubDate>
    </item>
    <item>
      <title>프로그래머스_예산 JAVA</title>
      <link>https://devhan.tistory.com/350</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4tGgc/btsMhNfSNo0/AByKa9MHcUxBcckw0ZAynk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4tGgc/btsMhNfSNo0/AByKa9MHcUxBcckw0ZAynk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4tGgc/btsMhNfSNo0/AByKa9MHcUxBcckw0ZAynk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4tGgc%2FbtsMhNfSNo0%2FAByKa9MHcUxBcckw0ZAynk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;259&quot; height=&quot;405&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;405&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;figure id=&quot;og_1738811063140&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12982&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/5P2Te/hyYb6fUJmy/9ms5M2UmWGKLtkWymHrtu1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12982&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12982&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/5P2Te/hyYb6fUJmy/9ms5M2UmWGKLtkWymHrtu1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 풀 문제는 '예산'이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에 풀었던 문제라 과거 코드가 남아있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;과거 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1738811068571&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public int solution(int[] d, int budget) {
        int answer = 0;
        
        Arrays.sort(d);
        
        if(d[0] &amp;gt; budget) return 0;
        
        for(int i = 0; i &amp;lt; d.length; i++) {
            if(budget &amp;gt;= d[i]) {
                budget -= d[i];
                answer++;
            }
        }
        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별로 고칠 것이 없어보여서,,,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 리팩토링만 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현재 코드&lt;/h3&gt;
&lt;pre id=&quot;code_1739327949644&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public int solution(int[] d, int budget) {
        int answer = 0;
        
        Arrays.sort(d);
        
        if(d[0] &amp;gt; budget) return answer;
    
        for(int cost : d) {
            if(budget &amp;lt; cost) return answer;
            budget -= cost;
            answer++;
        }
    
        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘도 즐거운 코딩..!&lt;/p&gt;</description>
      <category>알고리즘/알고리즘 풀이</category>
      <category>Java</category>
      <category>코딩테스트</category>
      <category>프로그래머스</category>
      <author>데부한</author>
      <guid isPermaLink="true">https://devhan.tistory.com/350</guid>
      <comments>https://devhan.tistory.com/350#entry350comment</comments>
      <pubDate>Thu, 13 Feb 2025 22:17:10 +0900</pubDate>
    </item>
    <item>
      <title>프로그래머스_기능개발 JAVA</title>
      <link>https://devhan.tistory.com/349</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cb8PhV/btsL8boFMfx/VMPykb5Z2gzBR6JHUuVVfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cb8PhV/btsL8boFMfx/VMPykb5Z2gzBR6JHUuVVfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cb8PhV/btsL8boFMfx/VMPykb5Z2gzBR6JHUuVVfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcb8PhV%2FbtsL8boFMfx%2FVMPykb5Z2gzBR6JHUuVVfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;259&quot; height=&quot;414&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;figure id=&quot;og_1738809413459&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42586&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42586&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42586&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 3차원 어쩌구 문제가 있었는데 프로그래머스 안에서 검색해보니 나오질 않아서 최신 문제들로 다시 추천해달라고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 오늘 풀 문제는 '기능개발'이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제는 처음 풀어보는 문제였다. 그래서 과거 코드는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 읽었을 땐 문제에서 뭔가 강조하는 것 같았던 '작업 일수'는 별로 신경 안 써도 될 거 같았는데..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니 문제를 풀다보니깐 '작업 일수'가 되게 중요한 거였다..!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 문제에 나오는 내용은 허튼 것이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드&lt;/h2&gt;
&lt;pre id=&quot;code_1738810319057&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public List&amp;lt;Integer&amp;gt; solution(int[] progresses, int[] speeds) {
        List&amp;lt;Integer&amp;gt; answer = new ArrayList&amp;lt;&amp;gt;();
        int success = 100;
        Deque&amp;lt;Integer&amp;gt; deque = new ArrayDeque&amp;lt;&amp;gt;();
        
        // 작업 종료일 구해서 deque에 넣기
        for(int i = 0; i &amp;lt; progresses.length; i++) {
            int progress = progresses[i];
            int speed = speeds[i];
            
            int endDate = (int)Math.ceil((double)(success - progress) / (double)speed);
            deque.offer(endDate);
        }
        
        int prevEndDate = deque.poll();
        int doneTaskCnt = 1;
        while(!deque.isEmpty())
        {
            if(prevEndDate &amp;gt;= deque.peek()) {
                deque.poll();
                doneTaskCnt++;
            } else {
                answer.add(doneTaskCnt);
                prevEndDate = deque.poll();
                doneTaskCnt = 1;
            }
            
            if(deque.isEmpty()) {
                answer.add(doneTaskCnt);
            }
        }
        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧게 설명해보자면 작업일수를 먼저 구해서 deque에 넣고 (기준 일수가 됨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 작업일수보다 다음 작업 일수가 같거나 작으면 같이 배포되니까 ++ 해주고 아니면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 된 기능 수를 answer 리스트에 넣고 작업일수 기준을 업데이트해줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근무지 점심시간에 풀은거라 보안 상 캡처가 안돼서;;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3점을 맞았는데 캡쳐본이 안올라간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘도 즐거운 코딩!!!&lt;/p&gt;</description>
      <category>알고리즘/알고리즘 풀이</category>
      <category>Java</category>
      <category>코딩테스트</category>
      <category>프로그래머스</category>
      <author>데부한</author>
      <guid isPermaLink="true">https://devhan.tistory.com/349</guid>
      <comments>https://devhan.tistory.com/349#entry349comment</comments>
      <pubDate>Thu, 6 Feb 2025 20:50:41 +0900</pubDate>
    </item>
  </channel>
</rss>