Archive

2021-02-22 TIL

|

2021-02-22 TIL


  • 오늘 한 것
    1. 리액트 공부 - 리액트를 영어강의로 무작정 시작해서 그런지 아직 개념이 붕붕 떠다니는 느낌이 든다. 익숙치 않은 영어로 뭔가를 배우기엔 내 영어실력이 아직은..ㅠ 공식문서든 책이든 유튜브든 시간날때마다 리액트 기본 원리에 대해 알아봐야겠다.
    2. 학원 대면수업(15:30~22:00) JSP - 오라클 데이터베이스와 JSP 연동하는 5가지 방법을 배웠다. 물론 방법이야 만드는 사람에따라 수십가지는 되겠지만, 내가 배운것은 크게 5가지로 날로 만들어 연동하는것과 ConnectionPool을 이용하는 방법, 자바빈즈를 이용하는 방법과 ConnectionPool과 자바빈즈를 모두 이용하는 방법, 그리고 외부 라이브러리를 이용해서 이미 만들어진 API를 이용해서 연동하는 방법이다.



  • 내일 할 것
    1. 리액트 공부
    2. 학원 대면수업(15:30~22:00) JSP



  • 끝으로

학원에서 배우는 것들도 점점 코딩양이 많아지고 있다. 내 공부도 따로 하려면 시간을 조금 더 내야겠다.

오늘의 한 줄 총평 : 알차게


JSP - 오라클 데이터베이스 연동

|

JSP - 오라클 데이터베이스 연동


JSP에서 오라클 데이터베이스와 연동을 해보자

우선 실습을 위해 TEMPMEMBER 테이블을 만들었다.

이 테이블은 ID, 비번, 이름, 회원번호, 이멜, 전번, 우편, 주소, 직업 등의 필드를 가진다.

오라클 JDBC는 WebContent/WEB-INF/lib 안에 모셔둔다. 외부 라이브러리는 모두 이 곳에 위치하게 된다.

  1. JDBC를 이용하여 데이터 가져오기

    • 이 방법은 가장 기초적이면서도 꼭 알아야하는 방법이다
    • java.sql.*을 import 시킨다
    <%
    	Class.forName("oracle.jdbc.driver.OracleDriver");
    	Connection conn = null;
    	Statement stmt = null;
    	ResultSet rs = null;
    	String id = "";
    	String passwd = "";
    	<!-- ...생략 -->
    	int counter = 0;
       	
    	try {
    		conn = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521/XEPDB1", "mytest", "mytest");
    		stmt = conn.createStatement();
    		rs = stmt.executeQuery("SELECT * FROM TEMPMEMBER");
    %>
    <!-- HTML 시작 -->
    <%
    		if(rs!=null) {
    			while(rs.next()) {
    				id= rs.getString("id");
    				passwd = rs.getString("passwd");
    				<!-- ...생략 -->
    %>
    			<tr>
    				<td><%= id %></td>
    				<td><%= passwd %></td>
    				<!-- ...생략 -->
    			</tr>
    <%
    				counter++;
    			}//end while
    		}// end if
    %>
    </table><br>
    <%
    	} catch(SQLException se) {
    		System.out.println("sql exception");
    	} catch(Exception e) {
    		System.out.println("exception");
    	} finally {
    		if(rs != null) try {rs.close();}catch(SQLException e){}
    		if(stmt != null) try {stmt.close();}catch(SQLException e){}
    		if(conn != null) try {conn.close();}catch(Exception e){}
    	}
    %>
    
    • Java에서 연동했던 방법과 크게 다르지 않다. 다만, 표현하고자 하는 데이터를 XML 태그 안에 집어넣는다는것 뿐?


  2. Connection Pool을 이용하여 데이터 가져오기

    • 데이터베이스와 연결된 커넥션을 미리 만들어서 저장해두었다가 필요시 꺼내쓰고 다시 반환하는 것을 커넥션 풀이라고 한다
    • 커넥션 풀을 이용하면 데이터베이스 부하를 줄이고 동시접속자를 유동적으로 처리할 수 있다는 장점이 있다
    • 커넥션을 저장할 수 있는 두 개의 Vector를 생성한다. (혹은 ArrayList)
    • Free 벡터는 커넥션풀 클래스의 객체가 생성될 때 미리 생성된 커넥션을 저장하는 저장소이다
    • used 벡터는 실제 미들웨어에서 DBMS와 연결할 때 사용하는 커넥션 저장소이다
    • Free에서 커넥션 객체를 꺼내 used에 저장하고 실제 앱과 연결 후 다시 반납하는 구조이다

    커넥션풀


    //Connection Pool Class
       
    import java.sql.*;
    import java.util.*;
       
    public class ConnectionPool {
    	// DB 드라이버 불러오기
    	static {
    		try {
    			Class.forName("oracle.jdbc.driver.OracleDriver");
    		} catch(ClassNotFoundException e) {
    			e.printStackTrace();
    		}
    	}
       	
    	// 초기 커넥션 저장
    	private ArrayList<Connection> free;
    	private ArrayList<Connection> used; // 사용중인 커넥션 저장
    	private String url = "jdbc:oracle:thin:@localhost:1521/XEPDB1";
    	private String user = "mytest";
    	private String passwd = "mytest";
    	private int initialCons = 10; // 초기 커넥션 수
    	private int maxCons = 20; // 최대 커넥션 수
    	private int numCons = 0; // 총 커넥션 수
    	private static ConnectionPool cp;
       		
    	private ConnectionPool() throws SQLException {
    		// 초기 커넥션 수만큼 ArrayList 생성
    		free = new ArrayList<Connection>(initialCons);
    		used = new ArrayList<Connection>(initialCons);
       		
    		// 초기 커넥션 수만큼 커넥션 생성
    		while(numCons < initialCons) {
    			addConnection();
    		}
    	}
           
        // 객체 생성 후 ConnectionPool 리턴
    	public static ConnectionPool getInstance() {
    		try {
    			if(cp == null) {
    				synchronized(ConnectionPool.class) {
    					cp = new ConnectionPool();
    				}
    			}
    		} catch(SQLException e) {
    			e.printStackTrace();
    		}
    		return cp;
    	}
       	
    	// free에 커넥션 객체를 저장
    	private void addConnection() throws SQLException {
    		free.add(getNewConnection());
    	}
       	
    	// 새로운 커넥션 생성
    	private Connection getNewConnection() throws SQLException {
    		Connection con = null;
    		try {
    			con = DriverManager.getConnection(url, user, passwd);
    		} catch(SQLException e) {
    			e.printStackTrace();
    		}
    		System.out.println("About to connect to"+ con);
    		++numCons; // 커넥션 생성될 때마다 커넥선 수 증가
    		return con;
    	}
       	
    	// free에 있는 커넥션을 used로 옮기는 작업
    	public synchronized Connection getConnection() throws SQLException {
    		// free에 커넥션이 없으면 maxCons만큼 커넥션 생성
    		if(free.isEmpty()) {
    			while(numCons < maxCons) {
    				addConnection();
    			}
    		}
    		Connection _con;
    		_con = free.get(free.size() -1);
    		free.remove(_con);
    		used.add(_con);
    		return _con;
    	}
       	
    	// used에 있는 커넥션을 free로 반납
    	public synchronized void releaseConnection(Connection _con) throws SQLException {
    		boolean flag = false;
    		if(used.contains(_con)) {
    			used.remove(_con);
    			numCons--;
    			flag = true;
    		} else {
    			throw new SQLException("ConnectionPool 없음");
    		}
    		try {
    			if(flag) {
    				free.add(_con);
    				numCons++;
    			} else {
    				_con.close();
    			}
    		} catch(SQLException e) {
    			try {
    				_con.close();
    			} catch(SQLException e2) {
    				e2.printStackTrace();
    			}
    		}
    	}	
    }
    
    • JSP에서 ConnectionPool class 사용
    <%
    	ConnectionPool pool = ConnectionPool.getInstance();
    	Connection conn = null;
    	Statement stmt = null;
    	ResultSet rs = null;
    	String id = "";
    	String passwd = "";
    	<!-- ...생략 -->
    	int counter = 0;
       	
    	try {
    		conn = pool.getConnection();
    		stmt = conn.createStatement();
    		rs = stmt.executeQuery("SELECT * FROM TEMPMEMBER");
    %>
    <!-- HTML 시작 -->
    <%
    		if(rs!=null) {
    			while(rs.next()) {
    				id= rs.getString("id");
    				passwd = rs.getString("passwd");
    				<!-- ...생략 -->
    %>
    			<tr>
    				<td><%= id %></td>
    				<td><%= passwd %></td>
                    <!-- ...생략 -->
    			</tr>
    <%
    				counter++;
    			}//end while
    		}// end if
    %>
    </table><br>
    <%
    	} catch(SQLException se) {
    		System.out.println("sql exception");
    	} catch(Exception e) {
    		System.out.println("exception");
    	} finally {
    		if(rs != null) try {rs.close();}catch(SQLException e){}
    		if(stmt != null) try {stmt.close();}catch(SQLException e){}
    		if(conn != null) try {pool.releaseConnection(conn);}catch(Exception e){}
    	}
    %>
    
    • 1번 방법과 크게 다르지 않은데, 연결과 반납을 ConnectionPool 클래스의 메소드를 이용한다는 점이 다르다.


  3. JavaBeans를 이용하여 데이터 가져오기

    • VO와 DAO를 만들어 JSP에서 사용하는 방법
    package jdbc;
       
    public class TempMemberVO {
    	private String id;
    	private String passwd;
    	//... 생략
    	public String getId() {
    		return id;
    	}
    	public void setId(String id) {
    		this.id = id;
    	}
    	public String getPasswd() {
    		return passwd;
    	}
    	public void setPasswd(String passwd) {
    		this.passwd = passwd;
    	}
        //... 생략
    }
    
    public class TempMemberDAO {
    	private final String JDBC_DRIVER = "oracle.jdbc.driver.OracleDriver";
    	private final String JDBC_URL = "jdbc:oracle:thin:@localhost:1521/XEPDB1";
    	private final String USER = "mytest";
    	private final String PASSWD = "mytest";
       	
    	public TempMemberDAO() {
    		try {
    				Class.forName(JDBC_DRIVER);
    		} catch(Exception e) {
    				System.out.println("ERROR : JDBC 드라이버 로딩 실패");
    		}
    	}
       	
    	// DB에서 데이터 받아서 벡터에 저장
    	public Vector<TempMemberVO> getMemberList() {
    		Connection conn = null;
    		Statement stmt = null;
    		ResultSet rs = null;
    		Vector<TempMemberVO> vecList = new Vector<TempMemberVO>();
    		try {
    			conn = DriverManager.getConnection(JDBC_URL, USER, PASSWD);
    			String query = "select * from tempmember";
    			stmt = conn.createStatement();
    			rs = stmt.executeQuery(query);
    			while(rs.next()) {
    				TempMemberVO vo = new TempMemberVO();
    				vo.setId(rs.getString("id"));
    				vo.setPasswd(rs.getString("passwd"));
    				<!-- ...생략 -->
    				vecList.add(vo);
    			}
    		} catch(Exception e) {
    			System.out.println("Exception" + e);
    		} finally {
    			if(rs != null) try {rs.close();}catch(SQLException e){}
    			if(stmt != null) try {stmt.close();}catch(SQLException e){}
    			if(conn != null) try {conn.close();}catch(Exception e){}
    		}
    		return vecList;
    	}
    }
    
    • JSP에서 사용
    <jsp:useBean id="dao" class="jdbc.TempMemberDAO" scope="page" />
    <%
    	Vector<TempMemberVO> vlist = dao.getMemberList();
    	int counter = vlist.size();
    	for(int i=0; i<vlist.size(); i++) {
    		TempMemberVO vo = vlist.elementAt(i);
    %>
    	<tr>
    		<td><%= vo.getId() %></td>
    		<td><%= vo.getPasswd() %></td>
    		<td><%= vo.getName() %></td>
    		<td><%= vo.getMem_num1() %></td>
    		<td><%= vo.getMem_num2() %></td>
    		<td><%= vo.getEmail() %></td>
    		<td><%= vo.getPhone() %></td>
    		<td><%= vo.getZipcode() %>/<%= vo.getAddress() %></td>
    		<td><%= vo.getJob() %></td>
    	</tr>
    <%
    	}
    %>
    </table>
    


  4. Connection Pool과 JavaBeans 모두 적용하는 방법

    • 2번과 3번의 방법을 혼용하여 사용
    • 3번의 DAO에서 연결,반납하는 방법을 커넥션 풀 객체를 생성하고 커넥션 풀의 메소드를 이용
    public class TempMemberDAO {
    	private ConnectionPool pool = null;
       	
    	public TempMemberDAO() {
    		try {
    			pool = ConnectionPool.getInstance();
    		} catch(Exception e) {
    			System.out.println("ERROR : 커넥션 얻어오기 실패");
    		}
    	}
       	
    	// DB에서 데이터 받아서 벡터에 저장
    	public Vector<TempMemberVO> getMemberList() {
    		Connection conn = null;
    		Statement stmt = null;
    		ResultSet rs = null;
    		Vector<TempMemberVO> vecList = new Vector<TempMemberVO>();
    		try {
    			conn = pool.getConnection();
    			String query = "select * from tempmember";
    			stmt = conn.createStatement();
    			rs = stmt.executeQuery(query);
    			while(rs.next()) {
    				TempMemberVO vo = new TempMemberVO();
    				vo.setId(rs.getString("id"));
    				vo.setPasswd(rs.getString("passwd"));
    				vo.setName(rs.getString("name"));
    				vo.setMem_num1(rs.getString("mem_num1"));
    				vo.setMem_num2(rs.getString("mem_num2"));
    				vo.setEmail(rs.getString("e_mail"));
    				vo.setPhone(rs.getString("phone"));
    				vo.setZipcode(rs.getString("zipcode"));
    				vo.setAddress(rs.getString("address"));
    				vo.setJob(rs.getString("job"));
    				vecList.add(vo);
    			}
    		} catch(Exception e) {
    			System.out.println("Exception" + e);
    		} finally {
    			if(rs != null) try {rs.close();}catch(SQLException e){}
    			if(stmt != null) try {stmt.close();}catch(SQLException e){}
    			if(conn != null) try {pool.releaseConnection(conn);}catch(Exception e) {}
    		}
    		return vecList;
    	}
    }
    
    • JSP에서 사용하는 방법은 3번의 방법과 동일하다


  5. WAS에서 제공하는 DBCP 이용하여 데이터 가져오기

    • 아파치에서 제공하는 Connection Pool인 DBCP를 이용하여 오라클과 연동한다
    • www.apache.org의 common에서 collections, dbcp, pool 라이브러리를 다운받아 jar파일을 WEB-INF/lib 폴더에 넣는다
    • META-INF에 context.xml 파일을 추가하여 다음 속성을 기입한다
    <?xml version="1.0" encoding="UTF-8"?>
    <Context>
    	<Resource name="jdbc/myOracle" 
                  auth="Container" 
                  driverClassName="oracle.jdbc.driver.OracleDriver" 
                  type="javax.sql.DataSource" 
                  url="jdbc:oracle:thin:@localhost:1521/XEPDB1" 
                  username="mytest" 
                  password="mytest" 
                  maxTotal="8" 
                  maxIdle="10" 
                  maxWaitMillis="-1" />
    </Context>
    

    속성

    자료 출처 : https://devbox.tistory.com/entry/JSP-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80-1

    • DD (web.xml)에 다음을 추가한다
      <resource-ref>
      	<description>ConnectionPool</description>
      	<res-ref-name>jdbc/myOracle</res-ref-name>
      	<res-type>javax.sql.DataSource</res-type>
      	<res-auth>Container</res-auth>
      </resource-ref>
    
    • JSP 컨테이너에서 이 환경설정을 사용해서 드라이버에 연결한다
    // import 주의
    import javax.naming.*;
    import javax.sql.*;
       
    public class TempMemberDAO {
    	private Connection getConnection() {
    		Connection conn = null;
    		try {
    			Context init = new InitialContext();
    			DataSource ds = (DataSource) init.lookup("java:comp/env/jdbc/myOracle");
    			conn = ds.getConnection();
    		} catch(NamingException ne) {
    			ne.printStackTrace();
    		} catch(SQLException se) {
    			se.printStackTrace();
    		}
    		return conn;
    	}
        	// DB에서 데이터 받아서 벡터에 저장
    	public Vector<TempMemberVO> getMemberList() {
    		Connection conn = null;
    		Statement stmt = null;
    		ResultSet rs = null;
    		Vector<TempMemberVO> vecList = new Vector<TempMemberVO>();
    		try {
    			conn = getConnection();
    			String query = "select * from tempmember";
    			stmt = conn.createStatement();
    			rs = stmt.executeQuery(query);
    			while(rs.next()) {
    				TempMemberVO vo = new TempMemberVO();
    				vo.setId(rs.getString("id"));
    				vo.setPasswd(rs.getString("passwd"));
    				vo.setName(rs.getString("name"));
    				vecList.add(vo);
    			}
    		} catch(Exception e) {
    			System.out.println("Exception" + e);
    		} finally {
    			if(rs != null) try {rs.close();}catch(SQLException e){}
    			if(stmt != null) try {stmt.close();}catch(SQLException e){}
    			if(conn != null) try {conn.close();}catch(Exception e){}
    		}
    		return vecList;
    	}
    }
    
    • 커넥션 객체는 외부 API를 이용해서 생성되고 나머지 로직은 이전 예제와 같다
    • 실행 모습

    실행



참고 자료


KG 아이티뱅크 강의 자료

처음해보는 JSP&Servlet 웹 프로그래밍

React.js Deploy

|

React.js Deploy


  1. Deployment Steps

    • Check Base-path : BrowserRouter의 basename을 확인하자, 생략가능
    • Build Project : npm run build 명령어로 빌드
    • Server must Always serve index.html : 라우팅은 정확하게
    • Upload Build Artifacts to Server : 전체 파일이 아닌 빌드된 폴더를 업로드하자


  2. Deploying on Firebase

    • npm install -g firebase-tools
    • firebase login 첫로그인시 인증 필요
    • firebase init 몇가지 질문들에 답한다

    질문

    Hosting 선택은 space bar이다… enter 누르면 error…

    질문2

    Project Setup은 설정하지 않고 Hosting Setup에서 public directory를 build로 줬다

    질문3

    남은 질문들에 답을 하고 init을 마친다

    • init으로 만들어진 firebasec와 firebase.json 파일을 수정하여 정보를 바꿀 수 있다
    • firebase deploy
    • 이때 firebasec의 default project명과 firebase에서 만든 프로젝트명이 같아야한다
    • 터미널에 출력된 Hosting URL로 접속해 테스트를 진행한다



참고 자료


reactjs.org - 공식홈페이지

Udemy - React The Complete Guide

React.js Test

|

React.js Test


  1. Test Rules

    • Don’t test the library : 라이브러리 자체를 테스트하지 마라 예를 들어, axios를 사용하는 경우 HTTP Request Fail이 일어난 경우 인터넷의 문제이지 axios 라이브러리의 문제가 아니라는 것
    • Don’t test complex connections : 복잡한 컴포넌트 관계를 테스트하지 마라 하나의 컴포넌트가 제대로 렌더링이 되는지, props가 잘 전달되는지 등을 테스트하되, 복잡하게 연결된 연결고리 자체를 테스트하지 말라
    • Do test isolated units : 고립된 유닛들을 테스트하라 리듀서의 function, 컴포넌트의 function 등을 테스트해라
    • Do test your conditional outputs : 조건부 산출물을 테스트하라 조건부로 화면이 나왔다 들어갔다 하는 것들을 테스트해라
    • 테스트에는 명확한 규칙이 없고 상황과 때에 따라 언제든 바뀔 수 있음을 명시


  2. Testing

    • AirBnB에서 배포한 enzyme, create-react-app에 내장된 jest 등 테스트 툴들이 있다
    • jest는 react만을 위한 테스트 툴이 아니라 javascript 테스트 툴이다
    • react-test-renderer 패키지는 react 컴포넌트를 순수 js 객체로 렌더링하는데 사용할 수 있는 렌더러를 제공한다. act export로 브라우저 환경과 비슷하게 테스트를 진행할 수 있다
    • enzyme-adapter-react-16 : enzyme을 사용하기 위한 라이브러리, react 17 공식 adapter는 아직 안나온듯하다
    • npm install –save-dev enzyme, react-test-renderer, enzyme-adapter-react-16
    import React from 'react';
    import { configure, shallow } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    import NavigationItems from './NavigationItems';
    import NavigationItem from './NavigationItem/NavigationItem';
       
    configure({ adapter: new Adapter() });
       
    describe('<NavigationItems />', () => {
      let wrapper;
       
      beforeEach(() => {
        wrapper = shallow(<NavigationItems />);
      });  
           
      it('인증되지 않은 사용자 접속시 2개의 <NavigationItem />이 렌더링 되어야함', () => {
        expect(wrapper.find(NavigationItem)).toHaveLength(2);
      });
           
      it('인증된 사용자 접속시 3개의 <NavigationItem />이 렌더링 되어야함', () => {
      	// wrapper = shallow(<NavigationItems isAuthenticated />)와 같다
        wrapper.setProps({ isAuthenticated: true });
        expect(wrapper.find(NavigationItem)).toHaveLength(3);
      });
    });
       
    
    • configure 메소드로 react 버전16을 사용하고 있음을 명시한다. enzyme은 react 15와 16에서 다르게 동작하기 때문
    • describe와 it은 jest의 메소드이다. describe는 하나의 테스트 그룹이고, it은 그 안의 작은 단위 테스트이다.
    • describe와 it의 첫번째 항목에 적은 문자열이 테스트 항목이 된다
    • enzyme에는 크게 3가지 메소드가 있다
      • shallow : 얕은 수준의 컴포넌트를 메모리 상에 렌더링, shallow, 단일 컴포넌트 테스트
      • mount : HOC나 자식 컴포넌트까지 전부 렌더링, deep, 다른 컴포넌트와의 관계
      • render : 컴포넌트를 정적 html로 렌더링, 브라우저에서 html로 어떻게 되는지 테스트
    • expect jest의 메소드로 테스트값과 예상값이 일치하는지 여부를 판단한다
    • 위의 코드의 첫번째 it은 NavigationItems가 2개의 NavigationItem을 갖는지를 판단했다
    • beforeEach() 메소드는 테스트 전 수행할 내용을 적는다. afterEach() 메소드도 있다
    • setProps(), setState()로 props나 state를 세팅하는 것도 가능하다


  3. Test Reducer

    • Redux의 action, connect, action creator의 모든 관계를 test하는 것은 비효율적이다
    • 라이브러리의 실행 자체는 테스트 대상이 아니다
    • 리듀서는 pure한 javascript function이므로 jest로 테스트가 가능하다
import reducer from './auth';
import * as actionTypes from '../actions/actionTypes';

describe('auth reducer', () => {
  it('should return initial state', () => {
    // 리듀서의 초기 state값 테스트
    expect(reducer(undefined, {})).toEqual({
      token: null,
      userId: null,
      error: null,
      loading: false,
      authRedirectPath: '/',
    });
  });

  it('should store token upon login', () => {
    // 로그인시 리듀서의 token 저장 여부 테스트
    expect(
      reducer(
        {
          token: null,
          userId: null,
          error: null,
          loading: false,
          authRedirectPath: '/',
        },
        { // 로그인 성공시 token,id 저장
          type: actionTypes.AUTH_SUCCESS,
          idToken: 'some-Token',
          userId: 'some-userId',
        }
      )
    ).toEqual({
      token: 'some-Token',
      userId: 'some-userId',
      error: null,
      loading: false,
      authRedirectPath: '/',
    });
  });
});


위는 간단한 사용법 예시이고 언제나 그렇듯, 공식문서를 참고하자

  • Jest Docs: https://facebook.github.io/jest/
  • Enzyme API: http://airbnb.io/enzyme/docs/api/



참고 자료


reactjs.org - 공식홈페이지

Udemy - React The Complete Guide

React 테스트 - zerocho

2021-02-21 TIL

|

2021-02-21 TIL


  • 오늘 한 것
    1. 리액트 공부 The complete Guide - 튜토리얼 프로젝트에 enzyme과 jest 테스팅 툴로 테스트를 만들어 진행하고 firebase 서버에 deploy하는 것까지 배웠다. 테스트 툴이 있다는 사실조차 처음 알았다. 혼자 공부를 하다보면 프로젝트를 하더라도 로컬에서 작동하는지만 확인하고 종료하기일쑤인데 직접 테스트를 하고 서버에 배포하는 작업까지 하는것은 처음이었다. 좋은 공부가 되었다.



  • 내일 할 것
    1. 리액트 공부
    2. 학원 대면수업(15:30~22:00) JSP



  • 끝으로

내일 다시 월요일

오늘의 한 줄 총평 : 힘차게