본문 바로가기

플젝일지

mn_project(轉 / 結)

 

 

 

 

 

 

 

 

백엔드 개발 (35일 소요)

> 담당 파트 : 로그인,간편로그인(네이버),회원가입, 보호자정보수정, 메일전송, 최근본병원

 

mn_project(轉) ①

▶ 구조 ▷ 백엔드 ▷ 프론트엔드 크게 백/ 프론트로 나눠서 구조 자체는 간단하게 설계했다 ▶ 개발진행 프론트 개발 (10일 소요) > 담당 파트 : 병원예약 캘린더 , 예약접수확인 , 펫 리스트 출력

geniem.tistory.com

 

이어서 

 

[ 간편로그인(네이버) ]

 

 

일반 로그인 이외에 요즘 어느 사이트에서나 다 쓰고있는 간편로그인기능을 추가하고 싶었다.

daumPost나 kakaoMap이 너무 간단하게 해결되어서 로그인API도 그럴줄 알았는데, 난이도가 좀 더 있었다.

 

 

네이버 로그인 API 명세 - LOGIN

네이버 로그인 API 명세 네이버 로그인 API는 네이버 로그인 인증 요청 API, 접근 토큰 발급/갱신/삭제 요청API로 구성되어 있습니다. 네이버 로그인 인증 요청 API는 여러분의 웹 또는 앱에 네이버

developers.naver.com

 

네이버에서 제공해주는 소스코드가 있어서 그걸 사용해보려고 시도해봤는데, token값을 가져오긴하는데 그이후의 프로세스가 진행이안됐다.

결국 다 실패하고 계속 찾아봤고,  더 간편한 방법이 있어서 그걸 차용했다.

 

pom.xml의 디펜던시에 scribejava를 추가한다!

<!-- naver -->
<dependency>
    <groupId>com.github.scribejava</groupId>
    <artifactId>scribejava-core</artifactId>
    <version>2.8.1</version>
</dependency>

 

파일구성

- LoginController

- NaverLoginBO

- NaverLoginAPI

 

 

일단 BO파일에서 네이버에서 받아온 Client ID값이나 Secret값, 내가 설정한 redirect URI를 등록해준다.

public class NaverLoginBO {
	private final static String CLIENT_ID = "{네이버에서 발급해준 ID}";
	private final static String CLIENT_SECRET = "{네이버에서 발급해준 SECRET}";
	private final static String REDIRECT_URI = "{내가 설정한 Redirect_URI}";
	private final static String SESSION_STATE = "oauth_state";
	// 프로필 조회 API URL
	private final static String PROFILE_API_URL = "https://openapi.naver.com/v1/nid/me";

 

요청한 URI로 넘어가서 값을 가져오는 메서드

public String getAuthorizational(HttpSession session) {
		// 세선 유효성 검증을 위한 난수 생성
		String state = generateRandomString();
		// 생성한 난수 값을 session에 저장
		setSession(session, state);
		// Scribe에서 제공하는 인증 URL 생성 기능을 이용하여 네아로 인증 URL 생성
		OAuth20Service oauthService = new ServiceBuilder().apiKey(CLIENT_ID).apiSecret(CLIENT_SECRET).callback(REDIRECT_URI)
				.state(state).build(NaverLoginAPI.instance());
		
		return oauthService.getAuthorizationUrl();
	}
	// 네이버 ID로 Callback 처리 및 AccessToken 획득 Method
	public OAuth2AccessToken getAccessToken(HttpSession session, String code, String state) throws IOException {
		// Callback으로 전달받은 세션 검증용 난수값과 세션에 저장되어 있는 값이 일치하는지 확인
		String sessionState = getSession(session);
		if(StringUtils.pathEquals(sessionState, state)) {
			OAuth20Service oauthService = new ServiceBuilder().apiKey(CLIENT_ID).apiSecret(CLIENT_SECRET).callback(REDIRECT_URI)
					.state(state).build(NaverLoginAPI.instance());

            // Scribe에서 제공하는 AccessToken 획득 기능으로 네아로 Access Token을 획득
            OAuth2AccessToken accessToken = oauthService.getAccessToken(code);
            return accessToken;
		}
		return null;
	}
	// 세션 유효성 검증을 위한 난수 생성기
	private String generateRandomString() {
		return UUID.randomUUID().toString();
	}
	// http session에 데이터 저장
	private void setSession(HttpSession session, String state) {
		session.setAttribute(SESSION_STATE, state);
	}
	// http session 에서 데이터 가져오기
	private String getSession(HttpSession session) {
		return (String) session.getAttribute(SESSION_STATE);
	}
	// Access Token을 이용하여 네이버 사용자 프로필 API를 호출
	public String getUserProfile(OAuth2AccessToken oauthToken) throws IOException {
		OAuth20Service oauthService = new ServiceBuilder()
				.apiKey(CLIENT_ID).apiSecret(CLIENT_SECRET).callback(REDIRECT_URI)
				.build(NaverLoginAPI.instance());
		OAuthRequest request = new OAuthRequest(Verb.GET, PROFILE_API_URL, oauthService);
		
		oauthService.signRequest(oauthToken, request);
		Response response = request.send();
		return response.getBody();
	}
}

 

 

NaverLoginAPI

import com.github.scribejava.core.builder.api.DefaultApi20;

public class NaverLoginAPI extends DefaultApi20 {
	
	protected NaverLoginAPI() {
	}
	private static class InstanceHolder {
		private static final NaverLoginAPI INSTANCE = new NaverLoginAPI();
	}
	static NaverLoginAPI instance() {
		return InstanceHolder.INSTANCE;
	}
	public String getAccessTokenEndpoint () {
		return "https://nid.naver.com/oauth2.0/token?grant_type=authorization_code";
	}
	protected String getAuthorizationBaseUrl() {
		return "https://nid.naver.com/oauth2.0/authorize";
	}	
}

 

 

로그인화면.jsp

<!-- 네이버 로그인 화면으로 이동 시키는 URL -->
<div id="naver_id_login" style="text-align:center"><a href="${url}">
<img width="50px" src="/am/resources/img/common/btnG_아이콘원형.png"/></a></div>

 

 

이렇게 해주면 네이버 로그인기능은 구현된다. 

DB에 저장할때 아이디중복부분이 신경쓰였는데,  service단에서 해결했다

 ObjectMapper mapper = new ObjectMapper();
      HashMap<String, Object> map = new HashMap<>();
      Map<String, Object> data = mapper.readValue(apiResult, Map.class);
      JSONParser jsonParser = new JSONParser();
      
      JSONObject jsonObject = (JSONObject) jsonParser.parse(apiResult);
      jsonObject = (JSONObject) jsonObject.get("response");
      map.put("cEmail", jsonObject.get("email"));
      map.put("cName", jsonObject.get("name"));
      map.put("cTel", jsonObject.get("mobile"));
      
      // 가져온 JSON형태의 data를 풀어서 리스트에 넣어주었다
      String email = (String) ((Map<String, Object>) data.get("response")).get("email");
      String id = email.split("@")[0] + "_naver"; // 아이디값 끝에 "_naver"를 넣어서 구별해줌
      customerDTO dto = new customerDTO();
      dto = cm.getCustomer(id);
      System.out.println("dto" + dto);
      String pwd = makeRandomPw();
      if (dto == null) { // 네이버 아이디로 회원가입된 정보가 없다면
         customerDTO ndto = new customerDTO();
         ndto.setcId(id);
         ndto.setcName(map.get("cName").toString());
         ndto.setcEmail(map.get("cEmail").toString());
         ndto.setcTel(map.get("cTel").toString());
         ndto.setcPw(encoder.encode(pwd));
         cm.register(ndto);
         return ndto;
      }
      return dto;
   }

 

 


 

 

[ 메일 전송 ]

 

메일전송은 사용자가 비밀번호찾기를 할때 직접 등록한 메일주소로 임시비밀번호를 만들어서 보낼때 사용한다.

우리는 google mail으로 메일보내기를 진행했다.

 

설정 -> 전달 및 POP/IMAP > IMAP 사용으로 변경하고 저장해주면 된다.

 

 

이제 2단계 인증을 해야한다. 보안으로 들어가서 2단계 인증을 들어가서 앱 비밀번호를 발급받는다.

 

일단 pom.xml 디펜던시 추가해준다

<!-- 이메일 -->
    <dependency>
        <groupId>javax.mail</groupId>
        <artifactId>javax.mail-api</artifactId>
        <version>1.5.4</version>
    </dependency>
    <dependency>
        <groupId>com.sun.mail</groupId>
        <artifactId>javax.mail</artifactId>
        <version>1.5.3</version>
    </dependency>

 

JAVA가 지원해주는 메일전송 기능이 여러가지가 있는데, 그중에서 JavaMailSender을 사용하기 위해서 디펜던시에 하나 더 추가해준다.

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>${org.springframework-version}</version>
</dependency>

이렇게 사용하면 mail을 MIME형식으로 보낼 수 있고, 사진이나 첨부파일을 같이 보낼수 있다.

 

 

MainConfig 파일을 만들어서 기본로직을 만들어주었다.

@Configuration
public class MailConfig {
	@Bean
	public static JavaMailSender mailSender() {
		JavaMailSenderImpl jms = new JavaMailSenderImpl();
		jms.setHost("smtp.gmail.com"); //google smtp 
		jms.setPort(587);
		jms.setUsername("내 계정@gmail.com"); // 보내는계정 
		jms.setPassword("발급받은 앱 비밀번호");
		
		Properties prop = new Properties();                 
        // 기본설정
        prop.setProperty("mail.transport.protocol", "smtp");    //메일전송 프로토콜 = smtp 
        prop.setProperty("mail.smtp.auth", "true");             //메일을 보낼 때 사용자 인증 OK
        prop.setProperty("mail.smtp.starttls.enable", "true");      // ttl이라는 암호화 방식을 설정 OK
        prop.setProperty("mail.debug", "true");             // 디버그 - 로그
        jms.setJavaMailProperties(prop); 

        return jms;

	}
}

 

비밀번호 찾기 controller

@PostMapping("customerSearchPw") // 비밀번호 찾기
public String customerSearchPw(@RequestParam String inputId, 
                            @RequestParam String inputName, 
                            @RequestParam String inputTel,HttpServletResponse res) {

    customerDTO dto = cs.customerSearchPw(inputId,inputName,inputTel);
    String tempPwd ="";
    if(dto !=null) {
        tempPwd = cs.makeRandomPw();
        int result = cs.customerPwChg(tempPwd,dto);
        if(result ==1) {
            String toMail = dto.getcEmail();
            String content = tempPwd;
            return "redirect:/customerSearchPw/"+toMail+"/"+content+"/";
        }
        System.out.println(tempPwd);
    }
    return "redirect:/customerSearchIdPw";

}

 

입력받은 값들 service단으로 넘겨서 검사해주고, 임시비밀번호 생성하는 service단으로 값 받아와서 content에 저장해준다.

 

 

mailController에서는 메일로 전송할 내용을 입력해준다.

>> 이왕에 MIME으로 쓰는거 첨부파일로 깔끔하게 보내고 싶었는데, 계속 실패해서 그냥 문자열로 보내는걸로 만족했다. (다음엔 반드시 !)

@RequestMapping(value="/customerSearchPw/{toMail}/{content}/", method=RequestMethod.GET)
String tempPwdSendMail(@PathVariable String toMail, @PathVariable String content, HttpServletResponse res, HttpServletRequest req)throws Exception{
    String title = "임시비밀번호 발급 메일입니다.";
    String pwd = content; 

    String msg = "AniMedi에서 발송된 메일입니다.\n\n";
    msg += "고객님의 임시 비밀번호는 " + pwd +" 입니다\n";
    msg += "로그인 하시고 비밀번호 변경을 해주세요!\n\n";
    msg += "오늘도 좋은 하루 보내세요~~~♥";

    mails.tempPwdSendMail(toMail,title,msg); 
                    // 받는사람 메일 / 제목 / 내용 >> service단으로 넘겨준다.
    req.setAttribute("msg","등록된 메일주소로 임시비밀번호 발송했습니다.");
    req.setAttribute("url","/am/customerLogin");
    return "am/common/alert";
}

 

 

mailService

package com.care.am.service.mail;

import javax.mail.internet.MimeMessage;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;

@Service
public class mailService {
	
	@Autowired JavaMailSender mailSender;
	
	public void tempPwdSendMail(String to,String subject,String body) {
		MimeMessage message = mailSender.createMimeMessage();
		try {
			MimeMessageHelper helper = new MimeMessageHelper(message, true, "utf-8");
			helper.setTo(to);
			helper.setSubject(subject);
			helper.setText(body);
			mailSender.send(message);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

controller에서 넘겨준 값 service에서 사용자에게 전송해준다! 

 

 

<< 이렇게 발송이 된다! 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

[ 최근 본 병원 ]

 

기존에 계획한 기능을 모두 구현하고도 약 3주정도 시간이 남아서 각자 1개의 파트씩 기능을 추가해서 구현하기로 했다.

그때 추가한 기능이 사용자가 병원예약하려고 병원을 조회하면 '최근본 상품'같이 '최근 본 병원' 을 출력해주는것이다.

 

대략적인 방법을 찾아보니 대체로 쿠키 아니면 세션을 사용해서 기능을 구현하고 있었는데,

두 가지 방법의 장/단점이 확실히 있었다.

 

session

장 : 보안에 강하다, 복잡한 데이터타입인 List 같은 자료구조를 그대로 session에 저장할 수 있고, 별도의 인코딩, 디코딩이 필요가 없다.

단 : 기능을 구현할 때 서버에 요청을 해야하기 때문에 사용자가 많아지면 서버에 부담을 줄 수 있다.

 

cookie

장 : 클라이언트에 저장하기 때문에 서버자원을 사용하지 않는다.

단 : 복잡한 데이터타입에 대한 인코딩,디코딩이 필요하다. 보안에 취약하다.

 

결론 

'최근본 병원'은 보안에 민감할 정보를 갖고있지 않고, 많은 데이터를 가져올 필요 없이 일종의 경로의 기능이라고 생각했기 때문에 서버에 부담이 없는 쿠키로 구현해보고 싶었다.

 

 

 

 

ⓐ view

사이드바모양으로 스크롤에 따라 움직이는 기능으로 만드려고 jQuery를 사용했다.

 

일단 jQuery UI를 추가해준다.

<script src="http://code.jquery.com/jquery-latest.min.js"></script>

 

기본문법

${'선택자').animate({속성:'값'}, 시간, 가속도 함수, 콜백 함수);

속도함수와 콜백함수는 생략이 가능!

 

 

<div class="whole_box">
    <b>최근 본 병원</b> 
    <div class="recently_box">
        <c:forEach items="${sessionScope.recentlyViewList}" var="view">
                <div>
                <form action="reservationForm/page" method="post">
                <input type="hidden" name="mediId" value="${view.m_id}" id="mId">
                <button type="submit"><img src="/am/resources/img/${view.m_photo}" width="100px" height="100px"><br>
                <b>${view.m_name}</b>
                </button>
                </form>
                </div>
            </c:forEach>
    </div>
</div>

 

기본 틀은 이렇게 잡고 애니메이션을 넣어준다.

<script type="text/javascript">
$(document).ready(function () {
	
    var tmp = parseInt($("#whole_box").css('top')); 	
    //css에서 설정한top의 값 가져와서 그값으로 위치를 잡는다.

    $(window).scroll(function () {
        var scrollTop = $(window).scrollTop();
        var box_position = scrollTop + tmp + "px";

        $("#whole_box").stop().animate({
            "top": box_position
        }, 500);
    }).scroll();
});
</script>

 

css

.whole_box{
    position: fixed;
    width: 120px;
    margin-right: -690px;
    top: 200px;
    right: 50%;
    text-align: center;
    height:500px;
}
.recently_box{
    width: 120px;
    position : absolute;
    margin-top:10px;
    height: 450px;
    border-radius: 20px;
	border: 2px solid #1b4c68;
}
.recently_box div:nth-child(1){
    padding-top:10px;
    height: 33.3%;
    border:1px solid #f5f5f5;
    border-radius: 20px;
}

(아마 기능이 안쓰이는 코드도 있을수있는데, 정리를 못했다 ㅠ)

 

 

ⓑ controller

 

사용자가 병원리스트 화면에서 병원을 클릭하면 병원id값을 Ajax로 controller에 넘겼다.

$.ajax({
        url: 'customer/recentlyView',
        type: 'POST',
        data: mediId,
        contentType : "application/json; charset=utf-8",
        success: function(response) {
        },
        error: function(error) {
        }
    });

 

 

postMapping으로 값을 받고

@PostMapping("customer/recentlyView") // 최근 본 병원
public void recentlyView(@RequestBody String mediId, HttpSession session,
        @CookieValue(value = "recentlyCookie", required = false) Cookie cookie, HttpServletResponse res,
        HttpServletRequest req) {
    String cId = session.getAttribute(LoginSession.cLOGIN).toString();
    int limitTime = 60 * 60 * 24; // 24시간
    SimpleDateFormat date = new SimpleDateFormat("HHmmss"); // 현재시간 포맷해서 가져오기
    String fd = date.format(new Date());
    Cookie viewCookie = new Cookie("cookie" + fd, cId + "/" + mediId); // 쿠키명에 현재시간을 넣어서 생성

    viewCookie.setPath("/"); // 경로를 최상위로 두어 모든곳에서 다 쓸수있게
    viewCookie.setMaxAge(limitTime);
    res.addCookie(viewCookie);
    cs.addrecentlyView(viewCookie.getValue());

}

 

쿠키자체를 생성하는것은 어렵지 않았는데, 출력할때 중복방지, 최신 항목을 가장 상단에 띄우기 위해 쿠키이름에 현재시간과 조회한 보호자 아이디 + 병원 아이디를 넣어 생성했다.

 

 

service에서 DB에 넣고, 가져올 데이터를 처리했다

// 최근 본 병원 추가
public void addrecentlyView(String mediCookie) { 
  recentlyViewDTO rvDTO = new recentlyViewDTO();
  String cId = mediCookie.split("/")[0];
  String mId = mediCookie.split("/")[1];
  rvDTO.setcId(cId);
  rvDTO.setmId(mId);
  cm.addRecentlyView(rvDTO); // db 값 저장
}

// 최근 본 병원 리스트 가져오기
public List<Map<String, String>> getRecentlyView(List<String> recentlyViewed, String cId) {
  List<Map<String, String>> getView = new ArrayList<Map<String, String>>();
  if (recentlyViewed.size() != 0) {
     getView = cm.getRecentlyView(cId);
     return getView;
  }
    return null;
}

 

DB의 값들은 24시간이내, 그리고 로그인상태이면 값이 유지되고, 로그아웃이 되면 값을 다 지웠다

계속 데이터를 쌓아둘 필요가없다고 생각했기때문에?!

// 최근 본 병원 데이터 삭제
public void delRecentlyView(String cId) {
  cm.delRecentlyView(cId);
}

 

 

'최근본병원'을 어디에 띄울까도 고민이었다.

페이지 자체가 사용자정보 / 병원리스트 크게 두개로 나눠있기 때문에 사용자정보쪽에서 접근을 쉽게하는게 좋다고 생각했고,

마이페이지의 메인페이지로 가져와서 띄웠다.

기존의 DB의 저장되어있던 사용자 정보를 DTO로 받아서 가져와서 MODEL로 출력해주는 기능에 

쿠키값을 분류하고 쿠키값에 맞는 값을 출력하는 코드를 추가했다. 

// 현재 요청의 모든 쿠키 가져오기
Cookie[] cookies = req.getCookies();
String cId = session.getAttribute(LoginSession.cLOGIN).toString();
System.out.println(cookies);

// 가져온 쿠키를 기반으로 최근에 본 병원 목록 생성
List<String> recentlyViewed = new ArrayList<>();
List<Map<String, String>> getViewList = new ArrayList<Map<String, String>>();
if (cookies != null) {
    for (Cookie cookie : cookies) {
        if (cookie.getName().startsWith("cookie")) {
            // "cookie"로 시작하는 쿠키의 값을 추출하여 목록에 추가
            recentlyViewed.add(cookie.getValue());
            // model.addAttribute("list",recentlyViewedHospitals);
        }
    }
    session.removeAttribute("recentlyViewList");
    getViewList = cs.getRecentlyView(recentlyViewed, cId);
    session.setAttribute("recentlyViewList", getViewList);
    // model.addAttribute("recentlyViewList", getViewList);


} else {
    System.out.println("null");
}

return "am/customer/customerInfo";
}

 

처음에는 쿠키값으로 가져온 병원리스트를 model로 출력해주고싶었는데, 그 값이 다른페이지로 로드하면 유지되지 않았다.

어쩔수없이 세션을 이용해서 값을 고정해두고 가져왔다

 

 

 

<= 그렇게 구현된 '최근 본 병원'

 

 

코드자체는 이미 자동로그인 할때 쿠키,세션을 다 사용해봤어서

그렇게 어렵다고 느끼지는 않았는데, 

쿠키값으로 DB 데이터를 가져오는것이나, 관리하는것 등등전반적인 프로세스를 짜는것에 조금 시간이 걸렸던것 같다.

 

 

 

 

 

 

 

 

 

 


 

 [ 프로젝트 후기 ]

 

이전에 node.js로 진행했던 프로젝트는 CRUD까지밖에 구현을 못했다면이번 프로젝트는 CRUD + 부가기능이 추가되어서 개인적으로 많은 공부가 되었다.

 

사실 기본중에 기본들을 구현해낸 수준이지만 그래도 갖춰야 할 기능을 거의 다 갖추고있다는점에 성취감을 느꼈다.

오픈API 간단한듯 어려운듯 - 잘만 사용하면 완성도 높은 기능을 구현할 수 있어서 다양하게 이용해보고 싶다!

 

 

 

 

 

 

 

 

프론트를 먼저 진행할때 나름의 재미가 있고 바로바로 결과물이 눈에 들어와서 성취감을 느껴서

내 적성은 프론트쪽일수도(?)라는 생각도 잠깐 했지만,

기능을 구현하고 전반적인 프로세스를 완성해내는 백에서의 성취감도 만만치 않다. ㅎㅎ

 

 

 

'플젝일지' 카테고리의 다른 글

mn_project(轉)  (0) 2023.11.30
mn_project (起 / 承)  (0) 2023.11.17