2019년 12월 25일 수요일

@ModelAttribute 어노테이션을 이용한 Controller에서 View로의 데이터 전달

@ModelAttribute가 Controller 메소드의 매개변수로 선언된 Command 객체의 긴 이름을 짦은 이름으로 변경할때도 사용되지만(해당 포스트는 여기를 클릭),
Controller 클래스에 있는 특정 데이터를 View(.jsp 페이지)에서 사용할수 있도록 View로 넘기는 용도로도 사용되는 별스런 역할도 할수 있다.
일단 개념부터 정리를 해 보면...

어떤 컨트롤러 클래스 안에있는 특정 메소드에 @ModelAttribute 어노테이션이 붙어 있으면 해당 컨트롤러 클래스의 모든 @RequestMapping 어노테이션이 붙은 메소드가 호출될 때마다 그 메소드 호출 전에 @ModelAttribute가 붙은 메소드가 일단 먼저 호출되고 그 이후 @RequestMapping이 붙은 메소드가 호출되는데 이때 @ModelAttribute 메소드 실행 결과로 리턴되는 객체(데이터)는 자동으로 @RequestMapping 어노테이션이 붙은 메소드의 Model에 저장이되고 그 이후에 .jsp(View)에서 @ModelAttribute 메소드가 반환한 데이터를 사용할수 있다.
놀라운 @ModelAttribute의 능력이랄까?

일단 코드에서 확인해 보자. 아래와 같은 컨트롤러 클래스가 있다.

@Controller
public class BoardController {
... 전 략 ...

//글 수정
@RequestMapping("/updateBoard.do")
public String updateBoard(BoardVO vo, BoardDAO bdDao) throws Exception {
System.out.println("UpdateBoardController 글 수정 처리~");

bdDao.updateBoard(vo);
return "getBoardList.do";
}

//글 상세 조회
@RequestMapping("/getBoard.do")
public String getBoard(BoardVO vo, BoardDAO bdDao, Model model) throws Exception {
System.out.println("GetBoardController 글 상세 조회 처리~");

model.addAttribute("boardModel", bdDao.getBoard(vo));
return "getBoard.jsp";
}

//글 목록 검색
@RequestMapping("/getBoardList.do")
public String getBoardList(
        @RequestParam(value="searchCondition", defaultValue="TITLE", required=true) String condition,
@RequestParam(value="searchKeyword", defaultValue="", required=false) String keyWord,
BoardVO vo,
BoardDAO bdDao,
Model model) throws Exception {
System.out.println("$$$$$$$ GetBoardListController 글 목록 검색 처리~");

model.addAttribute("boardListModel", bdDao.getBoardList(vo));
return "getBoardList.jsp";
}

@ModelAttribute("myModelAttribute")
public Map<String, String> joe() {
System.out.println("▶▶▶▶▶▶▶ 여기는 joe()~~~ ▶▶▶▶▶▶▶");

Map<String, String> infoMap = new HashMap<String, String>();

infoMap.put("joe", "Web Developer");
infoMap.put("kim", "Designer");
infoMap.put("nana", "CEO of M&P");

return infoMap;
}
}

위의 BoardController 클래스안에는 @RequestMapping 어노테이션이 붙은 메소드가 3개가 있는데
-. public String updateBoard()
-. public String getBoard()
-. public String getBoardList()

이들 메소드가 호출될때마다 @ModelAttribute("myModelAttribute") 어노테이션이 붙은 아래 메소드가 먼저 호출된다.
public Map<String, String> joe()
그리고 joe() 메소드에서 반환하는 데이터(infoMap)가 클라이언트 요청으로 실행될 @RequestMapping이 붙은 메소드의 Model 객체에 자동으로 저장이 된다. 이렇게 저장된 데이터는 .jsp 페이지에서 사용할수 있게 되는데 이때 @ModelAttribute("myModelAttribute") 안에 지정한 문자열인 myModelAttribute가 객체 이름이 된다. 
역시 .jsp에서 어떻게 데이터에 접근하는지 코드에서 확인해 보자.

예를들어 http://xxx.xxx.xx/getBoard.do로 요청이 들어오면 먼저  
public Map<String, String> joe()가 먼저 호출이 되고 그 이후 
public String getBoard(BoardVO vo, BoardDAO bdDao, Model model)가 호출이 되는데 이때 joe() 메소드에서 생성된 infoMap 데이터가 getBoard()의 model 객체에 자동으로 저장이 되고 getBoard.jsp에서는 다음과 같이 데이테어 접근할수 있게 된다. 

... 전 략 ...
<h1>글 상세보기</h1>
<a href="logout.do">로그아웃</a>
<hr>

구성원1 : ${ myModelAttribute.joe }<br/>
구성원2 : ${ myModelAttribute.nana }<br/>
<hr/>
... 후 략 ...

위 .jsp 페이지의 출력 결과는 다음과 같이 될 것이다.

구성원1 : Web Devloper
구성원2 : CEO of  M&P

혹은 다음과 같이도 할수 있다.

<c:forEach items="${ myModelAttribute }" var="item">
item.key : ${item.key }<br/>
item.value : ${item.value }<br/><br/>
</c:forEach>
<hr/>

그러면 다음과 같은 결과가 나올 것이다.

item.key : joe
item.value : Web Developer

item.key : nana
item.value : CEO of M&P

item.key : kim
item.value : Designer

@ModelAttribute 어노테이션이 이런 용도로도 사용될수 있다는 점이다.
그런데 @ModelAttribute는 이것 외에 또 있으니 @SessionAttribute와 연결되면 또 요술을 부린다.

2019년 12월 22일 일요일

컨트롤러 클래스에서의 return이 갖는 의미

아래와 같은 컨트롤러 클래스가 있을 경우 return "getBoardList.do"
혹은 return "getBoardList.jsp"가 갖는 의미가 무엇인가?

@Controller
public class InsertBoardController {

@RequestMapping(value="/insertBoard.do")
public String insertBoard(BoardVO vo, BoardDAO bdDao) {
System.out.println("InsertBoardController 글 등록 처리~");

bdDao.insertBoard(vo);

return "getBoardList.do";  ①
     //return "getBoardList.jsp";  ②
}
}

MVC에서 Controller는 기본적으로 Model에 대한 처리(DB로 부터 적정 정보 획득)와 이후 이동할 페이지 정보(View 정보)를 return 하는 역할이 Controller가 하는 역할이다.

따라서 return "getBoardList.do"가 갖는 의미도 Model에 대한 처리(bdDao.insertBoard(vo);)와 이후 이동할 페이지 정보 즉 View 정보를 반환하는 역할의 의미가 return "getBoardList.do";인 것이다.

따라서 Context Path가 member라고 한다면 return "getBoardList.do";의 실행은 http://localhost/member/getBoardList.do와 같은 url 형태의 request 요청이 발생하게 하는 역할이 return "getBoardList.do";의 의미이다.

return "getBoardList.jsp";의 경우는 /member/getBoardList.jsp를 막바로 호출하는 형태라고 한다면 return "getBoardList.do";의 경우는 getBoardList.do에 해당하는 특정 컨트롤러의 특정 메소드 실행후 그 특정 메소드가 지정하는 .jsp 페이지로의 이동을 의미하게 된다.

참고적으로 Controller 메소드가 return 하는 View 정보는 기본적으로 포워딩 방식으로 동작한다.  
따라서 사용자의 브라우저 주소줄에  
http://xxx.xxx.xx/member/insertBoard.do로 요청에 대해서  
return "getBoardList.do"; 해도  
사용자의 브라우저 주소줄은 http://xxx.xxx.xx/member/insertBoard.do로 변함이 없다.  
만일 포워딩이 아니라 리다이렉트 방식으로 동작하게 할려면  
return "redirect:getBoardList.do";와 같이 해야 한다.   
그러면 사용자의 브라우저 주소줄은 http://xxx.xxx.xx/member/getBoardList.do와 같이 변경되어 나타날 것이다.

2019년 12월 20일 금요일

ModelAndView의 setViewName() 메소드에 redirect: 사용하는 법

ModelAndView의 setViewName() 메소드에 redirect: 사용하는 법

ModelAndview는 Model 정보(DB로 부터 획득한 데이터 정보)와 View 정보(이동할 페이지의 .jsp 파일 정보)를 같이 담아서 넘기는 클래스인데 ViewResolver와 같이 엮이게 되고 그 중에서 ModelAndview.setViewName()에서 redirect:가 붙을 경우와 그렇지 않을 경우 .jsp 파일을 찾는 개념에 약간의 헛갈림이 있을수 있다. 이점에 대해서 정리하고자 한다.

상황 1. InternalResourceViewResolver의 prefix에 설정된 경로와 suffix의 설정 값이 다음과 같고

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/board/"></property>
<property name="suffix" value=".jsp"></property>
</bean>

상황 2. Context path가 다음과 같을 때(request.getContextPath()에서 출력된 정보가 다음과 같을 때)

/BordWebDay3Class05

따라서 full url은 다음과 같이 될 것이다.
http://localhost/BordWebDay3Class05/

상황 3. /webapp/WEB-INF/board/ 아래에 다음의 .jsp 파일들이 있다.
getBoard.jsp
getBoardList.jsp

ModelAndView.setViewName()에서 redirect:가 붙지 않으면 무조건 InternalResourceViewResolver가 설정한 prefix와 suffix 정보가 적용된 .jsp 파일을 찾고, redirect:가 붙으면 InternalResourceViewResolver 설정 정보는 무시되고 Context path 위치에서 .jsp 파일을 찾는다.
예를들어서

ModelAndView.setViewName("getBoardList");로 되어 있으면
http://localhost/BordWebDay3Class05/WEB-INF/board/getBoardList.jsp를 찾게 되고
(물론 /WEB-INF/board/getBoardList.jsp는 브라우저에서 직접 호출할수는 없다. 왜냐하면 /WEB-INF/ 아래에 있는 .jsp 파일은 브라우저에서 직접 호출이 안되기 때문이다)

ModelAndView.setViewName("redirect:getBoardList.jsp");로 되어 있으면
http://localhost/BordWebDay3Class05/getBoardList.jsp를 찾게 된다.

redirect:가 붙어있을 경우는 InternalResourceViewResolver가 동작하지 않고 그 반대는 InternalResourceViewResolver에 설정된 prefix 경로(정보)와 suffix 정보가 합쳐져서 .jsp 파일을 찾는다는 점을 기억하도록 하자.

만일 web.xml의 서블릿 url-pattern이 아래와 같은 상황 일때,
  <servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>*.do</url-pattern>
  </servlet-mapping>

ModelAndView.setViewName("getBoard.do");로의 View 설정의 경우는 ViewResolver가 동작하지 않도록 redirect:를 사용해야 한다. 그렇지 않으면 url이
/WEB-INF/board/xxx.do.jsp
와 같이 찾기 때문에 404 Not Found 에러가 발생하고 멈춰 서 버린다. 따라서 ModelAndView.setViewName("getBoard.do");와 같은 경우는 ViewResolver가 동작하지 않도록 redirect:를 사용해서 ModelAndView.setViewName("redirect:getBoard.do");와 같이 해야한다.

2019년 12월 10일 화요일

AOP around의 proceed() 메소드 동작에 대한 개념 정리

AOP around의 proceed() 메소드 동작에 대한 개념 정리 

AOP에서 Advice 메소드의 동작 시점(동작 방식)에는  
ㆍbefore : 비지니스 메소드 실행 전에 Advice 메소드 실행

ㆍafter-returning : 비지니스 메소드가 성공적으로 리턴되면 Advice 메소드 동작. 즉 비지니스 메소드가 성공적으로 실행되었을 경우에만 Advice 메소드 동작 

ㆍafter-throwing: 비지니스 메소드 실행중 예외가 발생할 경우 Advice 메소드 실행. 즉 비지니스 메소드가 실행에 실패했을 경우에만 Advice 메소드 실행 

ㆍafter : 비지니스 메소드의 성공 실패와 상관없이 비지니스 메소드 실행 후 무조건 Advice 메소드 동작 

ㆍaround : 비지니스 메소드 실행 전과 실행 후 Advice 메소드 동작하는 형태 

이렇게 5가지가 있는데 이 중에서 around의 proceed() 메소드의 기능에 대해서 정리하고자 한다.  

around 어드바이스의 경우는 클라이언트 호출을 가로챈다. 만일 around 어드바이스 메소드에서 막바로 return을 해 버리면 비지니스 메소드 자체가 실행이 안된다.  
따라서 around Advice 메소드에서 비지니스 메소드 호출에 대한 책임을 감당해야 한다. 즉 around Advice가 비지니스 호출을 가로챘기 때문에 around Advice에서 비지니스 호출을 해 주지 않으면 비지니스 메소드는 실행 될 길이 없다. 그런데 그렇게 할려면 비지니스 메소드에 대한 정보를 around Advice 메소드가 가지고 있어야 하는데 그 정보를 Srping 컨테이너가 around Advice 메소드로 넘겨준다. 그게 ProceedingJoinPoint 객체이다. 

around Advice 메소드는 다음과 같은 형태를 띤다. 여기서 비지니스 메소드로 진행하도록(proceed) 하는 메소드가 proceed() 메소드이다.  
이 메소드를 실행하기 전에 비지니스 메소드 호출 전에 처리할 코드를 수행하도록 하면 된다.  
proceed()를 기준으로 비지니스 메소드 수행 전과 후가 나뉘어 진다. 즉 proceed()가 호출 되기 전에는 
비지니스 메소드 호출 전이고 proceed()가 호출된 후에는 비지니스 메소드 호출 후라고 생각하면 된다. 

Object org.aspectj.lang.ProceedingJoinPoint.proceed() throws Throwable 

그런데 여기서 proceed() 메소드가 반환하는 값이 있는데 Object이다. 여기에 무엇이 담겨 있단 말인가? 
여기에는 비지니스 메소드가 실행한 후의 결과 값들이 담겨 있게 된다. 
예를 들어 비지니스 메소드의 기능이 select 기능이라고 한다면 그 결과 값(보통은 VO 형태에 담기고 이 VO가 Object에 담기게 된다)이 Object에 담기게 되고 insert와 같이 return 되는 값이 없을 경우에는 Object에는 null이 담기게 된다. 

public class AroundAdvice { 
    public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable { 
        String method = pjp.getSignature().getName(); 
        Object returnObj = pjp.proceed(); 

        if(returnObj != null) { 
              //아래에서 다음과 같은 내용 출력. 아래 UserVO는 
              //DB로부터 select 해서 return 된 값이다. 
              //UserVO [id=test, password=null, name=관리자, role=Admin] 
              System.out.println("returnObj.toString() : "+returnObj.toString());  
        

        return returnObj; 
    

2019년 12월 9일 월요일

AOP(Aspect Oriented Programming) 용어 정리

AOP(Aspect Oriented Programming) 용어 정리 

S/W에서 클래스간의(객체간의) 결합도가 높을 경우 그들 중 어느 한 클래스(객체)에 수정이 발생하면 해당 클래스(객체)가 사용된 모든 소스 코드를 수정해야 하고 이러한 수정은 S/W 유지 보수의 측면에서 뜻하지 않은 문제를 발생시키거나 유지 보수를 복잡하고 어렵게 만드는 요인이 된다. 따라서 가능한 객체들간의 결합도를 낮추는 방향으로 가야 한다. 

Spring에서 결합도를 낮추는 기법이 의존성 주입(IoC)과 AOP가 있는데 AOP의 용어를 명확히 핵심적으로 정리하고자 한다. 
IoC와 AOP는 개발자가 소스코드에서 해 주지 않아도 Spring 컨테이너가 알아서 처리해 주므로 인해 소스코드 수정을 하지 않아도 된다는 개념이 핵심이다. 그렇게 하도록 하기 위해 필요한 것이 Spring 설정 파일에서의 설정을 통해서 Spring 컨테이너가 처리하도록 하는 식이다. 


▶ 횡단 관심(Crosscutting Concerns) 
비지니스 메소드마다 공통으로 등장하는 코드를 의미(예외, 로깅, 트랜잭션같은 코드). 

▶ 핵심관심(Core Concerns) 
핵심 비지니스 로직을 의미. 

▶ Joinpoint 
모든 비지니스 메소드들을 의미 

▶ Pointcut 
모든 비지니스 메소드들 중에서 횡단 관심 코드를 수행하기 원하는 "특정 비지니스 메소드"를 의미 

▶ Advice  
횡단관심에 해당하는 코드를 담고 있는 메소드를 의미 

▶ Aspect 
Pointcut과 Advice의 결합(어떤 Pointcut 메소드에 대해 어떤 Advice 메소드를 실행할지를 정의) 

▶ Weaving  
Advice가 삽입되는 과정 

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 
비지니스 로직 메소드                            횡단관심 메소드 
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 
Joinpoint                                             Advice 
Pointcut 
                                Weaving 
                                 Aspect
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 

2019년 12월 8일 일요일

@Qualifier 어노테이션의 NoUniqueBeanDefinitionException 이슈 문제 해법

@Qualifier 어노테이션의 NoUniqueBeanDefinitionException 이슈 문제 해법

어노테이션을 이용하여 의존성을 주입하는 것 중에서 @Qualifier를 사용시 다음과 같은 에러가 발생하는 경우가 있다.

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [polymorphism.Speaker] is defined: expected single matching bean but found 2: apple,sony

에러 메시지에 나와 있는 것 처럼 의존성을 주입할 객체가 2개가 존재한다는 것이다(apple sony). 어떤 경우에서 이런 문제가 발생하는가?

public interface Speaker {
public void volumeUp();
public void volumeDown();
}

@Component("apple")
public class AppleSpeaker implements Speaker {

public AppleSpeaker() {
System.out.println("▶▶▶▷  Apple Speaker 생성자~~~ 객체 생성 ~~~");
}

             ... 중 략 ...
}

@Component("sony")
public class SonySpeaker implements Speaker {
public SonySpeaker() {
System.out.println("▶▶▶▷ SonySpeaker 생성자 ----- 객체 생성");
}

             ... 중 략 ...
}

위와 같을 경우 apple라는 id를 가진 AppleSpeaker 객체와 sony라는 id를 가진 SonySpeaker 객체를 Spring 컨테이너가 생성하게 된다.
이 두 객체는 둘 다 Speaker 타입의 인터페이스를 구현한 객체이다. 이를 경우 아래와 같이 Speaker 타입의 객체를 의존성 주입하고자 하면 Speaker 타입을 구현한 2개의 객체가 존재한다(AppleSpeaker, SonySpeaker). 따라서 아래의 speaker 변수에 어느 객체를 주입해야 할지 Spring 컨테이너가 결정할수가 없게된다. 따라서 위와 같은 에러를 발생하게 되는 것이다.
그런데 이 문제를 해결하기 위해 @Qualifier라는 어노테이션이 존재하는데(혹은 @Resource) 이 어노테이션을 아래와 같이 적용을 해도 여전히 아래와 같은 동일한 에러가 발생한다.

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [polymorphism.Speaker] is defined: expected single matching bean but found 2: apple,sony


@Component
public class LgTV implements TV {

@Autowired
@Qualifier("apple")
// @Resource(name="sony")
private Speaker speaker;

public LgTV() {
System.out.println("▶▶▶▷ LG TV 생성자 ----- 객체 생성 ---");
}

             ... 중 략 ...
}

이 이슈는 아마도 Spring 자체의 문제인듯하다(정확한건 잘 모르겠지만).
해결 하는 방법은 동일 타입의 객체가 여러개 있을 때 그 중 default로 주입할 클래스(혹은 최 우선 순위로 주입할 객체)를 선택할수 있게 하는 @Primary라는 어노테이션을 동일 타입의 클래스들 중 어느 하나에 지정해 주면 해결된다. 아래와 같이

@Primary
@Component("apple")
public class AppleSpeaker implements Speaker {

public AppleSpeaker() {
System.out.println("▶▶▶▷  Apple Speaker 생성자~~~ 객체 생성 ~~~");
}

             ... 중 략 ...
}

@Primary를 SonySpeaker 클래스나 AppleSpeaker 클래스느나 둘 중 어느 하나에 지정해 주면 문제가 깔끔하게 해결이 된다.

2019년 9월 27일 금요일

JavaScript를 이용하여 현재 브라우저의 종류 알아내기

브라우저가 어떤 종류인지를 확인하는 자바스크립트 코드이다.
jQuery에서 $.browser.chrome과 같은 방식은 deprecated되었기 때문에 아래와 같은 방식으로
확인할수 있다.
아래 코드는 브라우저가 크롬인지를 확인하는 코드이다.
다른 브라우저 체크도 원리는 동일하다.

var userAgent = window.navigator.userAgent.toLowerCase();

//크롬일 경우 isChrome에는 Chrome이라는 문잘의 위치 값이 반환되고 크롬이 아닐경우는
//-1이 반환된다. 나머지도 동일
var isChrome = userAgent.indexOf('chrome');
var isEdge = userAgent.indexOf('edge');
var isIE = userAgent.indexOf('trident');

if(isChrome > -1){
if(isEdge > -1){
//Edge는 Chrome과 Edge 모두의 값을 가지고 있기 때문에
alert("Edge 브라우저");
} else {
alert("Chrome 브라우저");
}
} else {
alert("Chrome이 아닙니다");
}

2019년 8월 22일 목요일

리눅스 터미널 창에서 혹은 Mac 터미널 창에서 ls 명령어로 디렉토리만 보기

리눅스 터미널 창에서 혹은 Mac 터미널 창에서 ls 명령어로 디렉토리만 보기 

ls 명령어는 대표적으로 가장 자주 사용하는 리눅스 명령어 일 것이다.  
그런데 특정 디렉토리가 있는지 확인하기 위해서 ls 명령어 뒤에 해당 디렉토리명을 입력하면 그 디렉토리 하위의 모든 파일들과 하위 디렉토리 및 그 하위 파일들을 한꺼번에 쏟아 낸다. 
#%^&#@*&#^@))(*&.....;;; 

이럴때 다음과 같은 방식으로 디렉토리의 존재 유무 등등 디렉토리만 보이게 할수 있다. 

$ ls -d ./Appli* 
⇒ 현재 위치 아래에 Appli*로 시작하는 디렉토리가 있는지 보여준다. 존재한다면 이름이 Appli로 시작하는 모든 디렉토리 목록을 보여준다. 이때 디렉토리에 대한 좀더 자세한 정보를 보고 싶다면 $ls -ld ./Appli*와 같이 할수도 있을 것이다. 

$ ls -d example/* 
⇒ example이라는 디렉토리 아래의 모든 디렉토리 이름들을 보여준다. 

터미널에서 의에로 디렉토리만 확인하고 싶을 때가 자주 있다. 아무튼... 

2019년 7월 4일 목요일

마우스 포인터 따라다니는 툴팁(Tooltip 말풍선) 만들기

아래와 같은 방식으로 동작하는 말풍선(Tooltip) 만들어 보고자한다.-. 특정 영역 범위 안에서 마우스 포인터 따라 툴팁도 같이 따라 움직인다이때 마우스의 x좌표의 값을 툴팁에 보여준다.-. 마우스 클릭(토글) 의해 툴팁이 보였다 사라졌다 되도록 한다.-. 툴팁의 모양은 화살표 모양이 아랫쪽으로 오는 형태마우스 윗쪽에 툴팁이 보이는 형태로 한다.-. 툴팁이 나타났을  Esc 키를 치면 툴팁이 사라지게 한다.

<html>
    <head>
        <title>Tooltip과 마우스 이벤트</title>
        <style>

        /* 툴팁을 보여줄 영역의 크기를 적당히 잡는다. 높이를 250px로 잡음. 
           display를 inline-block으로 하게되면 이 영역안에 있는 글자의 길이만큼 width가 잡힌다.
       만일 display: inline-block을 없애고 대신 width를 이용해서 가로 길이를 지정해도 된다.
       만일 display: inline-block을 없애고 width 값도 없으면 가로 길이는 현재 화면 크기만큼으로 설정된다.
       position: relative는 마우스와 툴팁이 어느 정도 간격으로 따라 다닐지를 지정하는데 사용
         */
        .tooltip {
            padding: 80px;
            height: 250px;
            background-color: #eeeeee;
            position: relative;
            display: inline-block;
            border-bottom: 1px dotted black;
        }

        /* 클래스명이 tootip인 엘리먼트 안에 클래스명 tooltiptext를 가진 엘리먼트를 지정한다. 
     아래는 한마디로 툴팁 박스 자체를 보이고자 하는 코드임
     width: 110px은 툴팁 박스의 가로 크기를 지정, background-color: black은 툴팁 박스의 색상을 검정색으로
        */
        .tooltip .tooltiptext {
            visibility: hidden; /* 최초에는 툴팁 박스 안 보이게 */
            width: 110px;
            background-color: black;
            color: #fff;
            text-align: center; /*툴팁 박스 안의 글자가 가운데로 모이도록 */
            border-radius: 6px; /* 툴팁 박스 모서리가 약간 둥글게 */
            padding: 5px 5; /* 위아래 padding을 5 좌우 패딩을 5로 */
            z-index: 55; /* 툴팁이 화면의 다른 요소들 위에 보이고 가려지지 않도록 하기 위해 */
            position: absolute; /* 툴팁 박스의 위치 지정 방식 설정 */
            left: 50%;
            bottom: 77%;
            margin-left: -60px; /* 툴팁 박스가 마우스 위치에 적당히 놓이도록 */
        }

        /* 툴팁 아래쪽 역 삼각형 형태의 화살표 모양 만들기. 개념은 상당히 웃기고 재미있다.
.tooltip .tooltiptext::after가 의미하는 바는 클래스명 tooltip 엘리먼트 안에있는
tooltiptext 엘리먼트 바로 뒤따라서(::before이면 바로 앞에) content가 지정하는 내용을 표시되게 하는데 
아래의 경우는 content 내용이 없으므로 아무것도 보여주지 않는다.
대신에 border-width: 5px을 하면 작은 4각형이 생긴다. 
그리고 border-color를 top right bottom left의 순으로 색상을 지정하는데 top의 색상을 black으로
나머지 right bottom left의 색상은 transparent으로 지정한다는 뜻인데 이렇게 지정하면
역삼각형 모양의 화살표가 생긴다. 개념 이해를 위해서 아래와 같이 해보면 알수 있다.
border-color: black red green cyan;
        */
        .tooltip .tooltiptext::after {
            content: "";
            position: absolute;
            color: green; /* content의 색깔*/
            top: 100%;
            left: 50%;
            margin-left: -5px; 
            border-width: 5px;
            border-style: solid;
            /* border-color의 Top Right Bottom Left의 색상 지정 */
            border-color: black transparent transparent transparent;
            /*border-color: black red blue cyan;*/
        }

        /* 아래 코드가 의미하는 것은 height를 250px로 잡았던 영역이 tootip이라는 이름의 div 영역인데
        이 영역 위에 마우스가 hover하면 클래스명 tooltip 엘리먼트 안에 있는 클래스명 
        tooltiptext 엘리먼트를 보이라는 뜻. 즉 클래스명 tooltiptext가 툴팁 박스 자체인데 
        tootip이라는 클래스명을 가진 엘리먼트(여기서는 div) 영역위에 마우스가 hover하면 
        툴팁 박스를 보이라는 뜻임 */
        .tooltip:hover .tooltiptext {
            visibility: visible;
        }
        </style>
    </head>
    <body>
        <h2>Top tooltip과 mouse move event</h2>
        <hr/>
        <br/>

        <!--툴팁을 보여줄 영역. css를 이용해서 이 영역의 크기를 적당히 잡는다. 
        클래스명 tooltip 영역은 마우스을 움직일 영역 클래스명 tooltiptext는 실제 툴팁 박스 자체-->
        <div class="tooltip">회색 영역 안에서 (1)마우스 움직이기, (2)마우스 클릭하기를 해 보세요. 
        마우스 클릭시 마다 툴팁이 보였다 사라졌다 할거예요
            <span class="tooltiptext">ID : 365</span>
        </div>

        <script>
        //마우스를 움직일 때 툴팁을 보여줄 영역
            var tooltip = document.getElementsByClassName("tooltip")[0];
            //툴팁 박스 
            var tooltipTxt = document.getElementsByClassName("tooltiptext")[0];

            // 아래 함수는 키보드 중 Esc 클릭시 툴팁이 사라지도록 하기
            document.onkeydown = function(e){
                var isEscape = false;

                if("key" in e){
                    console.log("e.key : " + e.key);
                    isEscape = (e.key === "Escape" || e.key === "Esc");
                } else {
                    isEscape = (e.keyCode === 27);
                }

                if(isEscape){
                //툴팁 박스를 사라지게
                    tooltipTxt.style.display = "none";
                }
            };

            //마우스 move 이벤트가 발생하면 
            tooltip.addEventListener('mousemove', function(e){
                // console.log("e.clientX : "+e.clientX);
                // console.log("e.clientY : "+e.clientY);

                //e.clientX의 값이 현재 위치의 마우스 포인터의 x 좌표 값
                //마우스를 움직이면 tooltipTxt(툴팁 박스)의 왼쪽 좌표를 마우스 포인터의 현재 x좌표로 지정
                tooltipTxt.style.left = (e.clientX - 15) + 'px';
                tooltipTxt.style.top = (e.clientY - 135) + 'px';
                //툴팁 박스의 높이를 지정
                tooltipTxt.style.height = "20px";
                //툴팁 박스 안에 표시될 글자를 마우스의 현재 위치 x 좌표 값
                tooltipTxt.innerHTML = "ID : " + e.clientX;
            });

            //마우스 클릭시마다 툴팁 박스가 보였다 사라졌다 하도록
            tooltip.addEventListener('click', function(e){
                if (tooltipTxt.style.display === 'none'){
                    tooltipTxt.style.display = "block";
                    console.log('툴팁이 보이지 않는 상태');
                } else {
                    tooltipTxt.style.display = "none";
                    console.log('툴팁이 보이고 있음~');
                }
            });
        
        </script>
    </body>

</html>