최원종의 개발 블로그

V13-4 마이페이지 소셜 이미지 출력하기 위한 코드 수정 본문

Spring boot 입문

V13-4 마이페이지 소셜 이미지 출력하기 위한 코드 수정

chl6698 2026. 6. 2. 11:30


 

User 엔티티 코드 확인

    // 머스태치 화면에서 사용할 편의 메서드 2
    // OAuthProvider 값에 따라서 경로 변수를 다르게 리턴
    public String getProfilePath() {
        if(this.profileImage == null) {
            return null;
        }
        // 이미지 경로가 http 로 시작 (소셜 가입)
        if(this.profileImage.startsWith("http")) {
            return this.profileImage;
        }
        // 로컬 이미지(서버 기준 경로)
        return "/images/" + this.profileImage;
    }

detail.mustache 추가

  <!--  회원 가입시 로컬 가입자                   -->
                    <img class="rounded-circle" src="{{user.profilePath}}" alt="프로필이미지"
                      style="width:200px; height:200px"/>
                <!--  회원 가입시 소셜 가입자               -->

detail.mustache 전체코드⬇️

더보기
{{> layout/header}}
<div class="container p-5 flex-grow-1">
    <div class="card">
        <div class="card-header">마이프로필</div>
        <div class="card-body">
            <!--  프로필 이미지  start  -->
            <div class="text-center mb-4">
                {{#user.profileImage}}
                <!--  회원 가입시 로컬 가입자                   -->
                    <img class="rounded-circle" src="{{user.profilePath}}" alt="프로필이미지"
                      style="width:200px; height:200px"/>
                <!--  회원 가입시 소셜 가입자               -->

                    <div class="mt-2">
                        <form action="/user/profile-image/delete" method="post">
                            <button type="submit" class="btn btn-sm btn-danger"
                            onclick="return confirm('프로필 사진을 삭제할까요?')">프로필 사진 삭제</button>
                        </form>
                    </div>
                {{/user.profileImage}}
                {{^user.profileImage}}
                    <div class="rounded-circle bg-secondary d-inline-flex
                                justify-content-center align-items-center border"
                                    style="width:200px; height:200px;">
                        <span>프로필 사진 없음</span>
                    </div>
                {{/user.profileImage}}
            </div>
            <!--  프로필 이미지   end -->
            <div class="mb-3">
                <label class="form-label fw-bold">사용자명</label>
                <p class="form-control-plaintext">{{user.username}}</p>
            </div>

            <div class="mb-3">
                <label class="form-label fw-bold">이메일</label>
                {{#user.email}}
                    <p class="form-control-plaintext">{{user.email}}</p>
                {{/user.email}}
            </div>

            <div class="mb-3">
                <label class="form-label fw-bold">가입일</label>
                <p class="form-control-plaintext">{{user.createdAt}}</p>
            </div>

            <div class="mb-3">
                <a href="/user/update-form" class="btn btn-primary" >회원정보 수정</a>
                <a href="/board/list" class="btn btn-secondary" >게시글 목록</a>
            </div>

        </div>
    </div>
</div>
{{> layout/footer}}

 


update-form.mustache 추가 코드

<img class="rounded-circle" src="{{user.profilePath}}" alt="프로필이미지"
                         style="width:200px; height:200px"/>

update-form.mustache 전체 코드⬇️​

더보기
{{> layout/header}}
<div class="container p-5 flex-grow-1">
    <div class="card">
        <div class="card-header"><b>회원정보수정</b></div>
        <div class="card-body">

            <!--  프로필 이미지  start  -->
            <div class="text-center mb-4">
                {{#user.profileImage}}
                    <img class="rounded-circle" src="{{user.profilePath}}" alt="프로필이미지"
                         style="width:200px; height:200px"/>
                    <div class="mt-2">
                        <form action="/user/profile-image/delete" method="post">
                            <button type="submit" class="btn btn-sm btn-danger"
                                    onclick="return confirm('프로필 사진을 삭제할까요?')">프로필 사진 삭제</button>
                        </form>
                    </div>
                {{/user.profileImage}}
                {{^user.profileImage}}
                    <div class="rounded-circle bg-secondary d-inline-flex
                                justify-content-center align-items-center border"
                         style="width:200px; height:200px;">
                        <span>프로필 사진 없음</span>
                    </div>
                {{/user.profileImage}}
            </div>
            <!--  프로필 이미지   end -->

            <!--  이미지 업로드 시 반드시   multipart/form-data 선언   -->
            <form action="/user/update" method="post" enctype="multipart/form-data">
                <div class="mb-3">
                    <input type="text" class="form-control" name="username" value="{{user.username}}" disabled>
                </div>
                <div class="mb-3">
                    <input type="password" class="form-control" name="password" value="" >
                </div>
                {{#user.email}}
                <div class="mb-3">
                    <input type="text" class="form-control" name="email" value="{{user.email}}" disabled>
                </div>
                {{/user.email}}

                <!--  프로필 이미지 등록 필드(선택 사항)  -->
                <div class="mb-3">
                    <label for="profileImage" class="form-label">프로필 사진(선택사항)</label>
                    <input type="file" id="profileImage" class="form-control" name="profileImage" accept="image/*"  >
                    <small class="form-text text-muted">이미지 파일만 업로드 가능합니다(JPG, PNG, GIF 등)</small>
                    <small class="form-text text-muted d-block mt-1">⭐ 프로필 사진을 등록하지 않아도 회원 가입 가능 </small>
                </div>

                <button class="btn btn-primary form-control">회원정보수정</button>
            </form>
        </div>
    </div>
</div>
{{> layout/footer}}

UserService 에서 소셜회원가입 메서드 따로 추가

/**
     * 소셜회원가입
     * @param joinDTO (사용자 회원가입 요청 정보)
     * @return User (저장된 사용자 정보)
     */
    @Transactional
    public User 소셜회원가입(UserRequest.JoinDTO joinDTO, String profileImageUrl) {
        log.info("소셜 회원가입 서비스 시작");
        // 회원가입시 사용자 이름 중복 체크
        userRepository.findByUsername(joinDTO.getUsername()).ifPresent(user -> {
            log.warn("회원가입 실패 - 중복된 사용자명 : {}", user.getUsername());
            throw new Exception400("이미 존재하는 사용자 이름입니다");
        });

        // 코드 수정
        User user = joinDTO.toEntity(profileImageUrl);
        String hashPwd = passwordEncoder.encode("1234");
        user.setPassword(hashPwd);
        // 기본 권한 추가 (일반 사용자로 설정)
        user.addRole(Role.USER);
        user.setOAuthProvider(OAuthProvider.KAKAO);
        return userRepository.save(user);
    }

UserController 코드 수정

// 1. 인가 코드 받음 -> 2. 토큰 발급 요청(JWT - CSR)
    @GetMapping("/kakao-redirect")
    public String kakaoCallback(@RequestParam(name = "code") String code, HttpSession session) {
        System.out.println("카카오 리다이렉트 값 확인 ");

        RestTemplate restTemplate1 = new RestTemplate();

        // 헤더
        HttpHeaders headers1 = new HttpHeaders();
        headers1.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");

        // 바디
        // 1. 방식  - application/json;
        // 2. 방식  - application/x-www-form-urlencoded;
        // {key=value, key=value, key=value} -> LinkedMultiValueMap -> 장점 - URLEncoding 을 알아서 해준다.
        LinkedMultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap();
        multiValueMap.add("grant_type", "authorization_code");
        multiValueMap.add("client_id", kakaoClientId);
        multiValueMap.add("redirect_uri", "http://localhost:8080/kakao-redirect");
        multiValueMap.add("code", code);
        // 최신사항 : 반드시 시크릿키 body 설정
        multiValueMap.add("client_secret", kakaoClientSecret);

        // 순서 중요 : 바디 + 헤더 결합 ( HTTP 요청 메세지 구축)
        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity(multiValueMap, headers1);


        // HTTP 요청 후 응답
        ResponseEntity<UserResponse.OAuthToken> response1 = restTemplate1.exchange(
                "https://kauth.kakao.com/oauth/token",
                HttpMethod.POST,
                request,
                UserResponse.OAuthToken.class
        );
        /// //////////////////////////////////////////////////////////
        // 발급 받은 액세스 토큰으로 해당 사용자의 정보 요청
        String accessToken = response1.getBody().getAccessToken();
        RestTemplate restTemplate2 = new RestTemplate();

        HttpHeaders headers2 = new HttpHeaders();
        // 주의! 반드시 Bearer + "공백한칸" + 토큰
        headers2.add("Authorization", "Bearer " + accessToken);
        headers2.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");

        HttpEntity request2 = new HttpEntity(headers2);

        // HTTP 요청 2
        ResponseEntity<UserResponse.KakaoProfile> response2 = restTemplate2.exchange(
                "https://kapi.kakao.com/v2/user/me",
                HttpMethod.POST,
                request2,
                UserResponse.KakaoProfile.class
        );

        ////  소셜 로그인 설계 방식
        //    1. 최초 사용자라면 우리 서버에 회원 가입 처리
        //    2. 회원 가입이 되어 있는 소셜 로그인 사용자라면 바로 로그인 처리

        // 소셜 가입자 닉네임 형태 결정  난수_최원종
        UserResponse.KakaoProfile.KakaoAccount.Profile profile = response2.getBody().getKakaoAccount().getProfile();
        String username = profile.getNickname() + "_" + response2.getBody().getId();
        User userEntity = userService.사용자이름조회(username);

        if (userEntity == null) {
            // 최소 사용자시 회원 자동 가입
            UserRequest.JoinDTO joinDTO = new UserRequest.JoinDTO();
            joinDTO.setUsername(username);
            joinDTO.setEmail(null);
            joinDTO.setPassword("1234");
            userEntity = userService.소셜회원가입(joinDTO, profile.getProfileImageUrl());

        }
        // 세션 정보 저장
        session.setAttribute(Define.SESSION_USER, userEntity);
        return "redirect:/board/list";
    }