├── webapp ├── WEB-INF │ ├── .gitignore │ └── web.xml ├── META-INF │ └── MANIFEST.MF ├── commons │ ├── _head.jspf │ └── _top.jspf ├── index.jsp ├── login.jsp ├── form.jsp └── stylesheets │ ├── main.css │ ├── docs.css │ └── bootstrap-responsive.css ├── .gitignore ├── src ├── net │ └── slipp │ │ ├── user │ │ ├── UserNotFoundException.java │ │ ├── PasswordMismatchException.java │ │ ├── web │ │ │ ├── LogoutServlet.java │ │ │ ├── CreateFormUserServlet.java │ │ │ ├── ApiFindUserServlet.java │ │ │ ├── UpdateFormUserServlet.java │ │ │ ├── LoginServlet.java │ │ │ ├── CreateUserServlet.java │ │ │ └── UpdateUserServlet.java │ │ ├── UserDAO.java │ │ └── User.java │ │ └── WebServerLauncher.java ├── core │ ├── jdbc │ │ ├── RowMapper.java │ │ ├── PreparedStatementSetter.java │ │ ├── ConnectionManager.java │ │ ├── DataAccessException.java │ │ └── JdbcTemplate.java │ ├── MyValidatorFactory.java │ ├── SessionUtils.java │ └── CharacterEncodingFilter.java └── main │ └── resources │ └── logback.xml ├── test ├── core │ └── ConnectionManagerTest.java └── net │ └── slipp │ └── user │ ├── JavaBean.java │ ├── JavaBeanUtilsTest.java │ ├── UserTest.java │ ├── UserDAOTest.java │ └── UserValidatorTest.java ├── deploy.sh ├── pom.xml └── README.md /webapp/WEB-INF/.gitignore: -------------------------------------------------------------------------------- 1 | /classes/ 2 | /lib/ 3 | -------------------------------------------------------------------------------- /webapp/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Class-Path: 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /tomcat.8080/ 3 | /target/ 4 | /.project 5 | /.settings/ 6 | /.classpath 7 | /bin/ 8 | -------------------------------------------------------------------------------- /src/net/slipp/user/UserNotFoundException.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user; 2 | 3 | public class UserNotFoundException extends Exception { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/net/slipp/user/PasswordMismatchException.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user; 2 | 3 | public class PasswordMismatchException extends Exception { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/core/jdbc/RowMapper.java: -------------------------------------------------------------------------------- 1 | package core.jdbc; 2 | 3 | import java.sql.ResultSet; 4 | import java.sql.SQLException; 5 | 6 | @FunctionalInterface 7 | public interface RowMapper { 8 | T mapRow(ResultSet rs) throws SQLException; 9 | } 10 | -------------------------------------------------------------------------------- /src/core/jdbc/PreparedStatementSetter.java: -------------------------------------------------------------------------------- 1 | package core.jdbc; 2 | 3 | import java.sql.PreparedStatement; 4 | import java.sql.SQLException; 5 | 6 | public interface PreparedStatementSetter { 7 | void setParameters(PreparedStatement pstmt) throws SQLException; 8 | } 9 | -------------------------------------------------------------------------------- /webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | slipp 4 | 5 | -------------------------------------------------------------------------------- /src/core/MyValidatorFactory.java: -------------------------------------------------------------------------------- 1 | package core; 2 | 3 | import javax.validation.Validation; 4 | import javax.validation.Validator; 5 | import javax.validation.ValidatorFactory; 6 | 7 | public class MyValidatorFactory { 8 | public static Validator createValidator() { 9 | ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); 10 | return factory.getValidator(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/core/ConnectionManagerTest.java: -------------------------------------------------------------------------------- 1 | package core; 2 | 3 | import static org.junit.Assert.assertNotNull; 4 | 5 | import java.sql.Connection; 6 | 7 | import org.junit.Test; 8 | 9 | import core.jdbc.ConnectionManager; 10 | 11 | public class ConnectionManagerTest { 12 | @Test 13 | public void connection() { 14 | Connection con = ConnectionManager.getConnection(); 15 | assertNotNull(con); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd slipp 4 | pwd 5 | 6 | git pull 7 | 8 | mvn clean package 9 | 10 | ~/next-workspace/devweb-workspace/apache-tomcat-8.0.14/bin/shutdown.sh 11 | 12 | rsync -avr --delete /Users/javajigi/next-workspace/devweb-workspace/apps/slipp/target/qna /Users/javajigi/next-workspace/devweb-workspace/apache-tomcat-8.0.14/webapps 13 | 14 | ~/next-workspace/devweb-workspace/apache-tomcat-8.0.14/bin/startup.sh -------------------------------------------------------------------------------- /src/core/SessionUtils.java: -------------------------------------------------------------------------------- 1 | package core; 2 | 3 | import javax.servlet.http.HttpSession; 4 | 5 | public class SessionUtils { 6 | public static boolean isEmpty(HttpSession session, String key) { 7 | Object object = session.getAttribute(key); 8 | if (object == null) { 9 | return true; 10 | } 11 | return false; 12 | } 13 | 14 | public static String getStringValue(HttpSession session, String key) { 15 | if (isEmpty(session, key)) { 16 | return null; 17 | } 18 | return (String)session.getAttribute(key); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /webapp/commons/_head.jspf: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | 4 | SLiPP 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/core/jdbc/ConnectionManager.java: -------------------------------------------------------------------------------- 1 | package core.jdbc; 2 | 3 | import java.sql.Connection; 4 | import java.sql.DriverManager; 5 | 6 | public class ConnectionManager { 7 | public static Connection getConnection() { 8 | String url = "jdbc:mysql://localhost:3306/slipp_dev?useUnicode=true&characterEncoding=utf8"; 9 | String id = "slipp"; 10 | String pw = "password"; 11 | 12 | try { 13 | Class.forName("com.mysql.jdbc.Driver"); 14 | return DriverManager.getConnection(url,id,pw); 15 | } catch (Exception e) { 16 | System.out.println(e.getMessage()); 17 | return null; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/net/slipp/WebServerLauncher.java: -------------------------------------------------------------------------------- 1 | package net.slipp; 2 | 3 | import java.io.File; 4 | 5 | import org.apache.catalina.startup.Tomcat; 6 | 7 | public class WebServerLauncher { 8 | public static void main(String[] args) throws Exception { 9 | String webappDirLocation = "webapp/"; 10 | Tomcat tomcat = new Tomcat(); 11 | tomcat.setPort(8080); 12 | 13 | tomcat.addWebapp("/", new File(webappDirLocation).getAbsolutePath()); 14 | System.out.println("configuring app with basedir: " + new File("./" + webappDirLocation).getAbsolutePath()); 15 | 16 | tomcat.start(); 17 | tomcat.getServer().await(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/core/jdbc/DataAccessException.java: -------------------------------------------------------------------------------- 1 | package core.jdbc; 2 | 3 | public class DataAccessException extends RuntimeException { 4 | 5 | public DataAccessException() { 6 | super(); 7 | } 8 | 9 | public DataAccessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 10 | super(message, cause, enableSuppression, writableStackTrace); 11 | } 12 | 13 | public DataAccessException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public DataAccessException(String message) { 18 | super(message); 19 | } 20 | 21 | public DataAccessException(Throwable cause) { 22 | super(cause); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/net/slipp/user/JavaBean.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user; 2 | 3 | public class JavaBean { 4 | 5 | String userName; 6 | String password; 7 | Integer id; 8 | 9 | public String getUserName() { 10 | return userName; 11 | } 12 | 13 | public void setUserName(String userName) { 14 | this.userName = userName; 15 | } 16 | 17 | public String getPassword() { 18 | return password; 19 | } 20 | 21 | public void setPassword(String password) { 22 | this.password = password; 23 | } 24 | 25 | public Integer getId() { 26 | return id; 27 | } 28 | 29 | public void setId(Integer id) { 30 | this.id = id; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/net/slipp/user/web/LogoutServlet.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user.web; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.ServletException; 6 | import javax.servlet.annotation.WebServlet; 7 | import javax.servlet.http.HttpServlet; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import javax.servlet.http.HttpSession; 11 | 12 | @WebServlet("/users/logout") 13 | public class LogoutServlet extends HttpServlet { 14 | @Override 15 | protected void doGet(HttpServletRequest req, HttpServletResponse response) 16 | throws ServletException, IOException { 17 | HttpSession session = req.getSession(); 18 | session.removeAttribute("userId"); 19 | response.sendRedirect("/"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webapp/index.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 | 3 | 4 | 5 | 6 | <%@ include file="./commons/_head.jspf" %> 7 | 8 | 9 | 10 | <%@ include file="./commons/_top.jspf" %> 11 | 12 |
13 |
14 |

SLiPP

15 |

Sustaninable Life, Programming, Programmer

16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 |

수정된 메인 페이지

24 |
25 |
26 |
27 |
28 | 29 | -------------------------------------------------------------------------------- /src/net/slipp/user/web/CreateFormUserServlet.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user.web; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.RequestDispatcher; 6 | import javax.servlet.ServletException; 7 | import javax.servlet.annotation.WebServlet; 8 | import javax.servlet.http.HttpServlet; 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | 12 | import net.slipp.user.User; 13 | 14 | @WebServlet("/users/createForm") 15 | public class CreateFormUserServlet extends HttpServlet { 16 | @Override 17 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) 18 | throws ServletException, IOException { 19 | req.setAttribute("user", new User()); 20 | RequestDispatcher rd = req.getRequestDispatcher("/form.jsp"); 21 | rd.forward(req, resp); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/net/slipp/user/JavaBeanUtilsTest.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import org.apache.commons.beanutils.BeanUtilsBean; 7 | import org.junit.Test; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public class JavaBeanUtilsTest { 12 | private static final Logger logger = LoggerFactory.getLogger(JavaBeanUtilsTest.class); 13 | 14 | @Test 15 | public void populate() throws Exception { 16 | final Map params = new HashMap<>(); 17 | params.put("userName", new String[]{"userA"}); 18 | params.put("password", new String[]{"secrect"}); 19 | params.put("id", new String[]{"10"}); 20 | final JavaBean javaBean = new JavaBean(); 21 | BeanUtilsBean.getInstance().populate(javaBean, params); 22 | logger.debug(javaBean.getUserName()); 23 | logger.debug(javaBean.getPassword()); 24 | logger.debug(javaBean.getId() + ""); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/core/CharacterEncodingFilter.java: -------------------------------------------------------------------------------- 1 | package core; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.Filter; 6 | import javax.servlet.FilterChain; 7 | import javax.servlet.FilterConfig; 8 | import javax.servlet.ServletException; 9 | import javax.servlet.ServletRequest; 10 | import javax.servlet.ServletResponse; 11 | import javax.servlet.annotation.WebFilter; 12 | 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | @WebFilter(urlPatterns={"/*"}) 17 | public class CharacterEncodingFilter implements Filter { 18 | private static final Logger logger = LoggerFactory.getLogger(CharacterEncodingFilter.class); 19 | 20 | @Override 21 | public void init(FilterConfig filterConfig) throws ServletException { 22 | logger.info("character encoding filter init!"); 23 | } 24 | 25 | @Override 26 | public void doFilter(ServletRequest request, ServletResponse response, 27 | FilterChain chain) throws IOException, ServletException { 28 | request.setCharacterEncoding("UTF-8"); 29 | chain.doFilter(request, response); 30 | } 31 | 32 | @Override 33 | public void destroy() { 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /webapp/commons/_top.jspf: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 4 | 5 | 32 | -------------------------------------------------------------------------------- /src/net/slipp/user/web/ApiFindUserServlet.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user.web; 2 | 3 | import java.io.IOException; 4 | import java.io.PrintWriter; 5 | 6 | import javax.servlet.ServletException; 7 | import javax.servlet.annotation.WebServlet; 8 | import javax.servlet.http.HttpServlet; 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | 12 | import net.slipp.user.User; 13 | import net.slipp.user.UserDAO; 14 | 15 | import com.google.gson.Gson; 16 | import com.google.gson.GsonBuilder; 17 | 18 | @WebServlet("/api/users/find") 19 | public class ApiFindUserServlet extends HttpServlet { 20 | @Override 21 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 22 | String userId = req.getParameter("userId"); 23 | if (userId == null) { 24 | resp.sendRedirect("/"); 25 | return; 26 | } 27 | 28 | UserDAO userDao = new UserDAO(); 29 | User user = userDao.findByUserId(userId); 30 | if (user == null) { 31 | return; 32 | } 33 | 34 | final GsonBuilder builder = new GsonBuilder(); 35 | builder.excludeFieldsWithoutExposeAnnotation(); 36 | final Gson gson = builder.create(); 37 | 38 | String jsonData = gson.toJson(user); 39 | resp.setContentType("application/json;charset=UTF-8"); 40 | 41 | PrintWriter out = resp.getWriter(); 42 | out.print(jsonData); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/net/slipp/user/UserTest.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user; 2 | 3 | import static org.junit.Assert.assertFalse; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | 9 | public class UserTest { 10 | public static User TEST_USER = new User("userId", "password", "name", "javajigi@slipp.net"); 11 | private UserDAO userDao; 12 | 13 | @Before 14 | public void setup() throws Exception { 15 | userDao = new UserDAO(); 16 | userDao.removeUser(TEST_USER.getUserId()); 17 | } 18 | 19 | @Test 20 | public void matchPassword() { 21 | assertTrue(TEST_USER.matchPassword("password")); 22 | } 23 | 24 | @Test 25 | public void notMatchPassword() { 26 | assertFalse(TEST_USER.matchPassword("password2")); 27 | } 28 | 29 | @Test 30 | public void login() throws Exception { 31 | User user = UserTest.TEST_USER; 32 | userDao.addUser(user); 33 | assertTrue(User.login(TEST_USER.getUserId(), TEST_USER.getPassword())); 34 | } 35 | 36 | @Test(expected=UserNotFoundException.class) 37 | public void loginWhenNotExistedUser() throws Exception { 38 | User.login("userId2", TEST_USER.getPassword()); 39 | } 40 | 41 | @Test(expected=PasswordMismatchException.class) 42 | public void loginWhenPasswordMismatch() throws Exception { 43 | User user = UserTest.TEST_USER; 44 | userDao.addUser(user); 45 | User.login(TEST_USER.getUserId(), "password2"); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/net/slipp/user/web/UpdateFormUserServlet.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user.web; 2 | 3 | import java.io.IOException; 4 | import java.sql.SQLException; 5 | 6 | import javax.servlet.RequestDispatcher; 7 | import javax.servlet.ServletException; 8 | import javax.servlet.annotation.WebServlet; 9 | import javax.servlet.http.HttpServlet; 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.servlet.http.HttpServletResponse; 12 | import javax.servlet.http.HttpSession; 13 | 14 | import net.slipp.user.User; 15 | import net.slipp.user.UserDAO; 16 | 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | import core.SessionUtils; 21 | 22 | @WebServlet("/users/updateForm") 23 | public class UpdateFormUserServlet extends HttpServlet { 24 | private static final Logger logger = LoggerFactory.getLogger(UpdateFormUserServlet.class); 25 | 26 | @Override 27 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 28 | HttpSession session = req.getSession(); 29 | String userId = SessionUtils.getStringValue(session, LoginServlet.SESSION_USER_ID); 30 | if (userId == null) { 31 | resp.sendRedirect("/"); 32 | return; 33 | } 34 | 35 | logger.debug("User Id : {}", userId); 36 | 37 | UserDAO userDao = new UserDAO(); 38 | User user = userDao.findByUserId(userId); 39 | req.setAttribute("isUpdate", true); 40 | req.setAttribute("user", user); 41 | RequestDispatcher rd = req.getRequestDispatcher("/form.jsp"); 42 | rd.forward(req, resp); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /webapp/login.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | 4 | 5 | 6 | 7 | <%@ include file="./commons/_head.jspf" %> 8 | 9 | 10 | 11 | <%@ include file="./commons/_top.jspf" %> 12 | 13 |
14 |
15 |
16 |
17 | 20 | 21 |
22 | 23 |
24 | 25 |
26 |
27 | 28 |
29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 |
37 | 38 |
39 |
40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 |
48 |
49 | 50 | -------------------------------------------------------------------------------- /test/net/slipp/user/UserDAOTest.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNull; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | import java.util.List; 8 | 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | public class UserDAOTest { 15 | private static final Logger logger = LoggerFactory.getLogger(UserDAOTest.class); 16 | 17 | private UserDAO userDao; 18 | 19 | @Before 20 | public void setup() { 21 | userDao = new UserDAO(); 22 | } 23 | 24 | @Test 25 | public void crud() throws Exception { 26 | User user = UserTest.TEST_USER; 27 | userDao.removeUser(user.getUserId()); 28 | userDao.addUser(user); 29 | User dbUser = userDao.findByUserId(user.getUserId()); 30 | assertEquals(user, dbUser); 31 | 32 | User updateUser = new User(user.getUserId(), "uPassword", "update name", "update@slipp.net"); 33 | userDao.updateUser(updateUser); 34 | dbUser = userDao.findByUserId(updateUser.getUserId()); 35 | assertEquals(updateUser, dbUser); 36 | } 37 | 38 | @Test 39 | public void 존재하지_않는_사용자_조회() throws Exception { 40 | User user = UserTest.TEST_USER; 41 | userDao.removeUser(user.getUserId()); 42 | User dbUser = userDao.findByUserId(user.getUserId()); 43 | assertNull(dbUser); 44 | } 45 | 46 | @Test 47 | public void findUsers() throws Exception { 48 | List users = userDao.findUsers(); 49 | assertTrue(users.size() > 0); 50 | logger.debug("Users : {}", users); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/net/slipp/user/UserDAO.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user; 2 | 3 | import java.util.List; 4 | 5 | import core.jdbc.JdbcTemplate; 6 | import core.jdbc.RowMapper; 7 | 8 | public class UserDAO { 9 | public void addUser(User user) { 10 | JdbcTemplate template = new JdbcTemplate(); 11 | String sql = "insert into USERS values(?,?,?,?)"; 12 | template.executeUpdate(sql, user.getUserId(), user.getPassword(), user.getName(), user.getEmail()); 13 | } 14 | 15 | public User findByUserId(String userId) { 16 | RowMapper rm = rs -> 17 | new User( 18 | rs.getString("userId"), 19 | rs.getString("password"), 20 | rs.getString("name"), 21 | rs.getString("email")); 22 | 23 | JdbcTemplate template = new JdbcTemplate(); 24 | String sql = "select * from USERS where userId = ?"; 25 | return template.executeQuery(sql, rm, userId); 26 | } 27 | 28 | public void removeUser(String userId) { 29 | JdbcTemplate template = new JdbcTemplate(); 30 | String sql = "delete from USERS where userId = ?"; 31 | template.executeUpdate(sql, userId); 32 | } 33 | 34 | public void updateUser(User user) { 35 | JdbcTemplate template = new JdbcTemplate(); 36 | String sql = "update USERS set password = ?, name = ?, email = ? where userId = ?"; 37 | template.executeUpdate(sql, user.getPassword(), user.getName(), user.getEmail(), user.getUserId()); 38 | } 39 | 40 | public List findUsers() { 41 | RowMapper rm = rs -> 42 | new User(rs.getString("userId"), 43 | rs.getString("password"), 44 | rs.getString("name"), 45 | rs.getString("email")); 46 | 47 | JdbcTemplate template = new JdbcTemplate(); 48 | String sql = "select * from USERS"; 49 | return template.list(sql, rm); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/net/slipp/user/web/LoginServlet.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user.web; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.RequestDispatcher; 6 | import javax.servlet.ServletException; 7 | import javax.servlet.annotation.WebServlet; 8 | import javax.servlet.http.HttpServlet; 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | import javax.servlet.http.HttpSession; 12 | 13 | import net.slipp.user.PasswordMismatchException; 14 | import net.slipp.user.User; 15 | import net.slipp.user.UserNotFoundException; 16 | 17 | @WebServlet("/users/login") 18 | public class LoginServlet extends HttpServlet { 19 | public static final String SESSION_USER_ID = "userId"; 20 | 21 | @Override 22 | protected void doPost(HttpServletRequest request, HttpServletResponse response) 23 | throws ServletException, IOException { 24 | String userId = request.getParameter("userId"); 25 | String password = request.getParameter("password"); 26 | 27 | try { 28 | User.login(userId, password); 29 | HttpSession session = request.getSession(); 30 | session.setAttribute(SESSION_USER_ID, userId); 31 | response.sendRedirect("/"); 32 | } catch (UserNotFoundException e) { 33 | forwardJSP(request, response, "존재하지 않는 사용자 입니다. 다시 로그인하세요."); 34 | } catch (PasswordMismatchException e) { 35 | forwardJSP(request, response, "비밀번호가 틀립니다. 다시 로그인하세요."); 36 | } 37 | } 38 | 39 | private void forwardJSP(HttpServletRequest request, 40 | HttpServletResponse response, String errorMessage) throws ServletException, IOException { 41 | request.setAttribute("errorMessage", errorMessage); 42 | RequestDispatcher rd = request.getRequestDispatcher("/login.jsp"); 43 | rd.forward(request, response); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/net/slipp/user/web/CreateUserServlet.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user.web; 2 | 3 | import java.io.IOException; 4 | import java.lang.reflect.InvocationTargetException; 5 | import java.util.Set; 6 | 7 | import javax.servlet.RequestDispatcher; 8 | import javax.servlet.ServletException; 9 | import javax.servlet.annotation.WebServlet; 10 | import javax.servlet.http.HttpServlet; 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | import javax.validation.ConstraintViolation; 14 | import javax.validation.Validator; 15 | 16 | import net.slipp.user.User; 17 | import net.slipp.user.UserDAO; 18 | 19 | import org.apache.commons.beanutils.BeanUtilsBean; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import core.MyValidatorFactory; 24 | 25 | @WebServlet("/users/create") 26 | public class CreateUserServlet extends HttpServlet { 27 | private static final Logger logger = LoggerFactory.getLogger(CreateUserServlet.class); 28 | 29 | @Override 30 | protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 31 | User user = new User(); 32 | try { 33 | BeanUtilsBean.getInstance().populate(user, request.getParameterMap()); 34 | } catch (IllegalAccessException | InvocationTargetException e1) { 35 | throw new ServletException(e1); 36 | } 37 | 38 | logger.debug("User : {}", user); 39 | 40 | Validator validator = MyValidatorFactory.createValidator(); 41 | Set> constraintViolations = validator.validate(user); 42 | if (constraintViolations.size() > 0) { 43 | request.setAttribute("user", user); 44 | String errorMessage = constraintViolations.iterator().next().getMessage(); 45 | forwardJSP(request, response, errorMessage); 46 | return; 47 | } 48 | 49 | UserDAO userDAO = new UserDAO(); 50 | userDAO.addUser(user); 51 | 52 | response.sendRedirect("/"); 53 | } 54 | 55 | private void forwardJSP(HttpServletRequest request, HttpServletResponse response, String errorMessage) throws ServletException, IOException { 56 | request.setAttribute("errorMessage", errorMessage); 57 | RequestDispatcher rd = request.getRequestDispatcher("/form.jsp"); 58 | rd.forward(request, response); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/net/slipp/user/web/UpdateUserServlet.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user.web; 2 | 3 | import java.io.IOException; 4 | import java.lang.reflect.InvocationTargetException; 5 | import java.util.Set; 6 | 7 | import javax.servlet.RequestDispatcher; 8 | import javax.servlet.ServletException; 9 | import javax.servlet.annotation.WebServlet; 10 | import javax.servlet.http.HttpServlet; 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | import javax.servlet.http.HttpSession; 14 | import javax.validation.ConstraintViolation; 15 | import javax.validation.Validator; 16 | 17 | import net.slipp.user.User; 18 | import net.slipp.user.UserDAO; 19 | 20 | import org.apache.commons.beanutils.BeanUtilsBean; 21 | 22 | import core.MyValidatorFactory; 23 | import core.SessionUtils; 24 | 25 | @WebServlet("/users/update") 26 | public class UpdateUserServlet extends HttpServlet { 27 | @Override 28 | protected void doPost(HttpServletRequest request, HttpServletResponse response) 29 | throws ServletException, IOException { 30 | HttpSession session = request.getSession(); 31 | String sessionUserId = SessionUtils.getStringValue(session, LoginServlet.SESSION_USER_ID); 32 | if (sessionUserId == null) { 33 | response.sendRedirect("/"); 34 | return; 35 | } 36 | 37 | User user = new User(); 38 | try { 39 | BeanUtilsBean.getInstance().populate(user, request.getParameterMap()); 40 | } catch (IllegalAccessException | InvocationTargetException e1) { 41 | throw new ServletException(e1); 42 | } 43 | 44 | if (!user.isSameUser(sessionUserId)) { 45 | response.sendRedirect("/"); 46 | return; 47 | } 48 | 49 | Validator validator = MyValidatorFactory.createValidator(); 50 | Set> constraintViolations = validator.validate(user); 51 | if (constraintViolations.size() > 0) { 52 | request.setAttribute("isUpdate", true); 53 | request.setAttribute("user", user); 54 | String errorMessage = constraintViolations.iterator().next().getMessage(); 55 | forwardJSP(request, response, errorMessage); 56 | return; 57 | } 58 | 59 | UserDAO userDAO = new UserDAO(); 60 | userDAO.updateUser(user); 61 | 62 | response.sendRedirect("/"); 63 | } 64 | 65 | private void forwardJSP(HttpServletRequest request, 66 | HttpServletResponse response, String errorMessage) throws ServletException, IOException { 67 | request.setAttribute("errorMessage", errorMessage); 68 | RequestDispatcher rd = request.getRequestDispatcher("/form.jsp"); 69 | rd.forward(request, response); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/net/slipp/user/UserValidatorTest.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.util.Iterator; 6 | import java.util.Set; 7 | 8 | import javax.validation.ConstraintViolation; 9 | import javax.validation.Validation; 10 | import javax.validation.Validator; 11 | import javax.validation.ValidatorFactory; 12 | 13 | import org.junit.BeforeClass; 14 | import org.junit.Test; 15 | 16 | public class UserValidatorTest { 17 | private static Validator validator; 18 | 19 | @BeforeClass 20 | public static void setUp() { 21 | ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); 22 | validator = factory.getValidator(); 23 | } 24 | 25 | @Test 26 | public void userIdIsNull() { 27 | User user = new User(null, "password", "name", ""); 28 | Set> constraintViolations = validator.validate(user); 29 | assertEquals(1, constraintViolations.size()); 30 | System.out.println(constraintViolations.iterator().next() 31 | .getMessage()); 32 | } 33 | 34 | @Test 35 | public void userIdLength() throws Exception { 36 | User user = new User("us", "password", "name", ""); 37 | Set> constraintViolations = validator.validate(user); 38 | assertEquals(1, constraintViolations.size()); 39 | System.out.println(constraintViolations.iterator().next() 40 | .getMessage()); 41 | 42 | user = new User("userIduserId2", "password", "name", ""); 43 | constraintViolations = validator.validate(user); 44 | assertEquals(1, constraintViolations.size()); 45 | System.out.println(constraintViolations.iterator().next() 46 | .getMessage()); 47 | } 48 | 49 | @Test 50 | public void email() throws Exception { 51 | User user = new User("user", "password", "name", "email"); 52 | Set> constraintViolations = validator.validate(user); 53 | assertEquals(1, constraintViolations.size()); 54 | System.out.println(constraintViolations.iterator().next() 55 | .getMessage()); 56 | } 57 | 58 | @Test 59 | public void invalidUser() throws Exception { 60 | User user = new User("us", "password", "name", "email"); 61 | Set> constraintViolations = validator.validate(user); 62 | assertEquals(2, constraintViolations.size()); 63 | Iterator> violations = constraintViolations.iterator(); 64 | while (violations.hasNext()) { 65 | ConstraintViolation each = violations.next(); 66 | System.out.println(each.getPropertyPath() + " : " + each.getMessage()); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /webapp/form.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | 4 | 5 | 6 | 7 | <%@ include file="./commons/_head.jspf" %> 8 | 9 | 10 | 11 | <%@ include file="./commons/_top.jspf" %> 12 | 13 |
14 |
15 |
16 |
17 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 |
33 | 34 |
35 |
36 |
37 | 38 |
39 | 40 | 41 | 42 | ${user.userId} 43 | 44 | 45 | 46 | 47 | 48 |
49 |
50 |
51 | 52 |
53 | 54 |
55 |
56 |
57 | 58 |
59 | 60 |
61 |
62 |
63 | 64 |
65 | 66 |
67 |
68 |
69 |
70 | 71 |
72 |
73 |
74 |
75 |
76 |
77 | 78 | -------------------------------------------------------------------------------- /src/core/jdbc/JdbcTemplate.java: -------------------------------------------------------------------------------- 1 | package core.jdbc; 2 | 3 | import java.sql.Connection; 4 | import java.sql.PreparedStatement; 5 | import java.sql.ResultSet; 6 | import java.sql.SQLException; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class JdbcTemplate { 11 | public void executeUpdate(String sql, PreparedStatementSetter pss) throws DataAccessException { 12 | Connection conn = null; 13 | PreparedStatement pstmt = null; 14 | try { 15 | conn = ConnectionManager.getConnection(); 16 | pstmt = conn.prepareStatement(sql); 17 | pss.setParameters(pstmt); 18 | 19 | pstmt.executeUpdate(); 20 | } catch (SQLException e) { 21 | throw new DataAccessException(e); 22 | } finally { 23 | try { 24 | if (pstmt != null) { 25 | pstmt.close(); 26 | } 27 | 28 | if (conn != null) { 29 | conn.close(); 30 | } 31 | } catch (SQLException e) { 32 | throw new DataAccessException(e); 33 | } 34 | } 35 | } 36 | 37 | public void executeUpdate(String sql, Object... parameters) { 38 | executeUpdate(sql, createPreparedStatementSetter(parameters)); 39 | } 40 | 41 | public T executeQuery(String sql, RowMapper rm, PreparedStatementSetter pss) { 42 | List list = list(sql, rm, pss); 43 | if (list.isEmpty()) { 44 | return null; 45 | } 46 | return list.get(0); 47 | } 48 | 49 | public T executeQuery(String sql, RowMapper rm, Object... parameters) { 50 | return executeQuery(sql, rm, createPreparedStatementSetter(parameters)); 51 | } 52 | 53 | public List list(String sql, RowMapper rm, PreparedStatementSetter pss) throws DataAccessException { 54 | Connection conn = null; 55 | PreparedStatement pstmt = null; 56 | ResultSet rs = null; 57 | try { 58 | conn = ConnectionManager.getConnection(); 59 | pstmt = conn.prepareStatement(sql); 60 | pss.setParameters(pstmt); 61 | 62 | rs = pstmt.executeQuery(); 63 | 64 | List list = new ArrayList(); 65 | while (rs.next()) { 66 | list.add(rm.mapRow(rs)); 67 | } 68 | return list; 69 | } catch (SQLException e) { 70 | throw new DataAccessException(e); 71 | } finally { 72 | try { 73 | if (rs != null) { 74 | rs.close(); 75 | } 76 | 77 | if (pstmt != null) { 78 | pstmt.close(); 79 | } 80 | 81 | if (conn != null) { 82 | conn.close(); 83 | } 84 | } catch (SQLException e) { 85 | throw new DataAccessException(e); 86 | } 87 | } 88 | } 89 | 90 | public List list(String sql, RowMapper rm, Object... parameters) { 91 | return list(sql, rm, createPreparedStatementSetter(parameters)); 92 | } 93 | 94 | private PreparedStatementSetter createPreparedStatementSetter(Object... parameters) { 95 | return new PreparedStatementSetter() { 96 | @Override 97 | public void setParameters(PreparedStatement pstmt) throws SQLException { 98 | for (int i = 0; i < parameters.length; i++) { 99 | pstmt.setObject(i + 1, parameters[i]); 100 | } 101 | } 102 | }; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /webapp/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | /* Jumbotrons 2 | -------------------------------------------------- */ 3 | 4 | /* Base class 5 | ------------------------- */ 6 | .jumbotron { 7 | position: relative; 8 | padding: 40px 0; 9 | color: #fff; 10 | text-align: center; 11 | text-shadow: 0 1px 3px rgba(0,0,0,.4), 0 0 30px rgba(0,0,0,.075); 12 | background: #020031; /* Old browsers */ 13 | background: -moz-linear-gradient(45deg, #020031 0%, #6d3353 100%); /* FF3.6+ */ 14 | background: -webkit-gradient(linear, left bottom, right top, color-stop(0%,#020031), color-stop(100%,#6d3353)); /* Chrome,Safari4+ */ 15 | background: -webkit-linear-gradient(45deg, #020031 0%,#6d3353 100%); /* Chrome10+,Safari5.1+ */ 16 | background: -o-linear-gradient(45deg, #020031 0%,#6d3353 100%); /* Opera 11.10+ */ 17 | background: -ms-linear-gradient(45deg, #020031 0%,#6d3353 100%); /* IE10+ */ 18 | background: linear-gradient(45deg, #020031 0%,#6d3353 100%); /* W3C */ 19 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#020031', endColorstr='#6d3353',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */ 20 | -webkit-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); 21 | -moz-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); 22 | box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); 23 | } 24 | .jumbotron h1 { 25 | font-size: 80px; 26 | font-weight: bold; 27 | letter-spacing: -1px; 28 | line-height: 1; 29 | } 30 | .jumbotron p { 31 | font-size: 24px; 32 | font-weight: 300; 33 | line-height: 30px; 34 | margin-bottom: 30px; 35 | } 36 | 37 | /* Link styles (used on .masthead-links as well) */ 38 | .jumbotron a { 39 | color: #fff; 40 | color: rgba(255,255,255,.5); 41 | -webkit-transition: all .2s ease-in-out; 42 | -moz-transition: all .2s ease-in-out; 43 | transition: all .2s ease-in-out; 44 | } 45 | .jumbotron a:hover { 46 | color: #fff; 47 | text-shadow: 0 0 10px rgba(255,255,255,.25); 48 | } 49 | 50 | /* Download button */ 51 | .masthead .btn { 52 | padding: 14px 24px; 53 | font-size: 24px; 54 | font-weight: 200; 55 | color: #fff; /* redeclare to override the `.jumbotron a` */ 56 | border: 0; 57 | -webkit-border-radius: 6px; 58 | -moz-border-radius: 6px; 59 | border-radius: 6px; 60 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 61 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 62 | box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 63 | -webkit-transition: none; 64 | -moz-transition: none; 65 | transition: none; 66 | } 67 | .masthead .btn:hover { 68 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 69 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 70 | box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 71 | } 72 | .masthead .btn:active { 73 | -webkit-box-shadow: inset 0 2px 4px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.1); 74 | -moz-box-shadow: inset 0 2px 4px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.1); 75 | box-shadow: inset 0 2px 4px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.1); 76 | } 77 | 78 | 79 | /* Pattern overlay 80 | ------------------------- */ 81 | .jumbotron .container { 82 | position: relative; 83 | z-index: 2; 84 | } 85 | .jumbotron:after { 86 | content: ''; 87 | display: block; 88 | position: absolute; 89 | top: 0; 90 | right: 0; 91 | bottom: 0; 92 | left: 0; 93 | opacity: .4; 94 | } 95 | 96 | label.error { 97 | color:orange; 98 | } -------------------------------------------------------------------------------- /src/net/slipp/user/User.java: -------------------------------------------------------------------------------- 1 | package net.slipp.user; 2 | 3 | import javax.validation.constraints.NotNull; 4 | import javax.validation.constraints.Size; 5 | 6 | import org.hibernate.validator.constraints.Email; 7 | 8 | import com.google.gson.annotations.Expose; 9 | 10 | public class User { 11 | @Expose 12 | @NotNull 13 | @Size(min = 4, max = 12) 14 | private String userId; 15 | 16 | @Expose(serialize = false) 17 | @NotNull 18 | @Size(min = 4, max = 12) 19 | private 20 | String password; 21 | @Expose 22 | @NotNull 23 | @Size(min = 2, max = 10) 24 | private String name; 25 | 26 | @Expose 27 | @Email 28 | private String email; 29 | 30 | public User(String userId, String password, String name, String email) { 31 | super(); 32 | this.userId = userId; 33 | this.password = password; 34 | this.name = name; 35 | this.email = email; 36 | } 37 | 38 | public User() { 39 | } 40 | 41 | public String getUserId() { 42 | return userId; 43 | } 44 | 45 | public void setUserId(String userId) { 46 | this.userId = userId; 47 | } 48 | 49 | public String getPassword() { 50 | return password; 51 | } 52 | 53 | public void setPassword(String password) { 54 | this.password = password; 55 | } 56 | 57 | public String getName() { 58 | return name; 59 | } 60 | 61 | public void setName(String name) { 62 | this.name = name; 63 | } 64 | 65 | public String getEmail() { 66 | return email; 67 | } 68 | 69 | public void setEmail(String email) { 70 | this.email = email; 71 | } 72 | 73 | public boolean matchPassword(String newPassword) { 74 | return this.password.equals(newPassword); 75 | } 76 | 77 | public boolean isSameUser(String newUserId) { 78 | if (this.userId == null) { 79 | return false; 80 | } 81 | 82 | return this.userId.equals(newUserId); 83 | } 84 | 85 | public static boolean login(String userId, String password) throws UserNotFoundException, PasswordMismatchException { 86 | UserDAO userDAO = new UserDAO(); 87 | User user = userDAO.findByUserId(userId); 88 | if (user == null) { 89 | throw new UserNotFoundException(); 90 | } 91 | 92 | if (!user.matchPassword(password)) { 93 | throw new PasswordMismatchException(); 94 | } 95 | 96 | return true; 97 | } 98 | 99 | @Override 100 | public int hashCode() { 101 | final int prime = 31; 102 | int result = 1; 103 | result = prime * result + ((email == null) ? 0 : email.hashCode()); 104 | result = prime * result + ((name == null) ? 0 : name.hashCode()); 105 | result = prime * result 106 | + ((password == null) ? 0 : password.hashCode()); 107 | result = prime * result + ((userId == null) ? 0 : userId.hashCode()); 108 | return result; 109 | } 110 | 111 | @Override 112 | public boolean equals(Object obj) { 113 | if (this == obj) 114 | return true; 115 | if (obj == null) 116 | return false; 117 | if (getClass() != obj.getClass()) 118 | return false; 119 | User other = (User) obj; 120 | if (email == null) { 121 | if (other.email != null) 122 | return false; 123 | } else if (!email.equals(other.email)) 124 | return false; 125 | if (name == null) { 126 | if (other.name != null) 127 | return false; 128 | } else if (!name.equals(other.name)) 129 | return false; 130 | if (password == null) { 131 | if (other.password != null) 132 | return false; 133 | } else if (!password.equals(other.password)) 134 | return false; 135 | if (userId == null) { 136 | if (other.userId != null) 137 | return false; 138 | } else if (!userId.equals(other.userId)) 139 | return false; 140 | return true; 141 | } 142 | 143 | @Override 144 | public String toString() { 145 | return "User [userId=" + userId + ", password=" + password + ", name=" 146 | + name + ", email=" + email + "]"; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | net.slipp 5 | qna 6 | 1.0 7 | war 8 | 9 | 10 | 8.0.15 11 | 12 | 13 | 14 | 15 | junit 16 | junit 17 | 4.11 18 | test 19 | 20 | 21 | com.google.code.gson 22 | gson 23 | 2.3 24 | 25 | 26 | 27 | ch.qos.logback 28 | logback-classic 29 | 1.1.2 30 | 31 | 32 | 33 | org.hibernate 34 | hibernate-validator 35 | 5.1.3.Final 36 | 37 | 38 | 39 | jstl 40 | jstl 41 | 1.2 42 | 43 | 44 | 45 | mysql 46 | mysql-connector-java 47 | 5.1.34 48 | 49 | 50 | 51 | commons-beanutils 52 | commons-beanutils 53 | 1.9.2 54 | 55 | 56 | 57 | javax.servlet 58 | javax.servlet-api 59 | 3.1.0 60 | provided 61 | 62 | 63 | 64 | org.apache.tomcat.embed 65 | tomcat-embed-core 66 | ${tomcat.version} 67 | provided 68 | 69 | 70 | org.apache.tomcat.embed 71 | tomcat-embed-logging-juli 72 | ${tomcat.version} 73 | provided 74 | 75 | 76 | org.apache.tomcat.embed 77 | tomcat-embed-jasper 78 | ${tomcat.version} 79 | provided 80 | 81 | 82 | org.apache.tomcat 83 | tomcat-jasper 84 | ${tomcat.version} 85 | provided 86 | 87 | 88 | org.apache.tomcat 89 | tomcat-jasper-el 90 | ${tomcat.version} 91 | provided 92 | 93 | 94 | org.apache.tomcat 95 | tomcat-jsp-api 96 | ${tomcat.version} 97 | provided 98 | 99 | 100 | 101 | 102 | qna 103 | 104 | src 105 | test 106 | webapp/WEB-INF/classes 107 | target/test-classes 108 | 109 | 110 | 111 | org.apache.maven.plugins 112 | maven-compiler-plugin 113 | 3.1 114 | 115 | 1.8 116 | 1.8 117 | utf-8 118 | 119 | 120 | 121 | maven-eclipse-plugin 122 | 2.9 123 | 124 | true 125 | 2.0 126 | / 127 | 128 | 129 | 130 | org.apache.maven.plugins 131 | maven-dependency-plugin 132 | 2.9 133 | 134 | ${project.basedir}/webapp/WEB-INF/lib 135 | false 136 | false 137 | true 138 | 139 | 140 | 141 | org.apache.maven.plugins 142 | maven-war-plugin 143 | 144 | webapp 145 | webapp/WEB-INF/web.xml 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 이 과정은 Servlet/JSP로 사용자 관리 시스템을 구현하면서 자바 웹 애플리케이션 개발자가 반드시 알아야할 빌드 도구, logging 라이브러리, validation, 리팩토링 등에 대해 다룬다. 2 | 3 | > 이 저장소 코드를 구현하는 전체 과정은 https://goo.gl/86ai5a 에서 볼 수 있다. 4 | 5 | > 이 저장소의 소스 코드를 활용하는 방법은 https://youtu.be/IgT_Zq16Mfg 동영상에서 참고할 수 있다. 6 | 7 | # 개발 환경 세팅 및 Servlet 기본 8 | ### [자바 웹 개발 환경 세팅](https://youtu.be/DNGp6Q9vLwk) 동영상 9 | * JDK 설치 10 | * Eclipse JEE Developers 버전 설치 11 | 12 | ### [Embedded Tomcat 버전 설치 및 설정](https://youtu.be/jWVlAclnIXo) 동영상 13 | * 웹 애플리케이션을 개발한 후 배포하려면 서버가 필요하다. 이 서버 중의 하나가 tomcat이다. tomcat은 오픈 소스이고, 무료이기 때문에 자바 기반으로 웹 애플리케이션을 개발할 때 많이 사용한다. 14 | * eclipse에서 tomcat 서버를 세팅하고 실행하는 과정을 다룬다. 15 | 16 | ### [Servlet으로 Hello World 출력하기](https://youtu.be/xCXw8xmmWC4) 동영상 17 | * Servlet은 동적인 웹 페이지를 구현하기 위한 기술 중의 하나이다. 사용자 요청에 "Hello World!"를 응답으로 보내주는 Servlet을 추가하고 실행하는 과정을 다룬다. 18 | 19 | ### [eclipse relaunch 플러그인 설치](https://youtu.be/OdCR6Y4_HAQ) 동영상 20 | * eclipse에서 tomcat 서버로 개발할 때 java 소스 코드가 변경되면 서버를 재시작해야 한다. 그런데 서버를 재시작하는 것이 은근히 불편한 과정이다. 이 불편한 과정을 해소하는 방법 중 하나가 relaunch 플러그인을 활용하는 방법이다. 21 | 22 | ### [Client to Server, Server to Client로 데이터 전달](https://youtu.be/RmOD3UZ3VkM) 동영상 23 | * Client와 Server 사이에 데이터를 전달할 필요가 있다. Client에서 HTTP Get 방식으로 데이터를 전달하고, Client에서 전달한 데이터를 Server에서 받아 처리한 후 이 데이터를 활용해 Client에 응답으로 보낼 메시지를 생성하는 과정을 다룬다. 24 | 25 | ### [Servlet Container와 Servlet 관계](https://youtu.be/aP4Lw3SfffQ) 동영상 26 | * Servlet은 자바 진영에서 동적인 웹 페이지를 개발하기 위한 기술 중의 하나이다. Servlet Container는 Servlet 인스턴스를 생성하고, Servlet을 실행하는 역할을 한다. 오픈 소스 이면서 가장 많이 사용하는 Servlet Container 중의 하나가 Tomcat이다. 27 | 28 | # 회원가입 페이지 분석 및 JSP 29 | 최신 개발 환경으로 실습할 수 있도록 지원하기 위해 개발 환경 세팅과 소스 코드가 지금까지 진행한 코드 다르다. 이후의 동영상은 찍은 시점이 다르다. 따라서 동영상에 약간의 차이가 있을 수 있다. 하지만 기본적인 사용방법은 같다. 특히 소스 코드가 상당히 많은 분량이 추가될 수 있고, 디렉토리 구조가 다를 수 있다. 이 부분은 https://youtu.be/IgT_Zq16Mfg 동영상을 참고해 세팅한다. 30 | 31 | ### [회원가입 페이지 요구사항 분석](https://youtu.be/d6k-WJpS2d4) 동영상 32 | * jsp & servlet 학습을 위해 앞으로 개발할 애플리케이션 요구사항 분석 33 | * 메인 페이지와 회원 가입 페이지 jsp 공유 34 | * web.xml의 welcome-file-list 설명 35 | 36 | ### [Servlet에서 JSP로 전환](https://youtu.be/JE2hNFniPJU) 동영상 37 | * Servlet의 문제점 인식 및 JSP 등장 이유 38 | * JSP와 Servlet의 관계 39 | * JSP에서 자바 코드 사용 40 | 41 | ### [JSP include, get/post](https://youtu.be/KGl70HWfw6w) 동영상 42 | * JSP 중복을 include 구문을 활용해 제거 43 | * get/post method 차이점 설명 44 | * 클라이언트에서 서버로 데이터 전달 및 데이터 추출 45 | 46 | ### [데이터 저장](https://youtu.be/7Zyp79is_jY) 동영상 47 | * 회원가입 데이터 저장을 위한 Database 객체 생성 48 | * 사용자 데이터 저장을 위한 User 객체 생성 49 | * sendRedirect를 활용한 페이지 이동 50 | 51 | # 회원가입 기능 구현 및 JSP2 52 | ### [단위 테스트 기반 개발](https://youtu.be/P3cYfVMdFHY) 동영상 53 | * 단위 테스트를 활용하지 않을 때의 개발/테스트 과정 설명 54 | * test source folder 추가 55 | * Database와 User 객체에 대한 단위 테스트 코드 추가 56 | * 로그인 기능에 대한 단위 테스트 추가 57 | 58 | ### [세션(session)을 활용한 로그인 기능 구현](https://youtu.be/hQulJST8C2c) 동영상 59 | * 정상적인 로그인 기능 구현 60 | * 로그인 성공 후 session에 정보 저장 61 | * 로그 아웃 기능 구현 62 | 63 | ### [예외 처리 및 forward](https://youtu.be/ANhfLsFAmas) 동영상 64 | * jsp에 예외 처리하는 흐름 이해 65 | * RequestDispatcher.forward()를 활용한 jsp 이동 66 | 67 | ### [JSP 문제점 및 MVC 기본](https://youtu.be/EfsVt7-ol_I) 동영상 68 | * JSP만으로 구현했을 때의 이슈 공유 69 | * 이 문제를 해결하기 위한 역할 분리로 MVC 등장 70 | * JSP와 Servlet을 병행해서 구현 71 | 72 | ### [redirect와 forward의 차이](https://youtu.be/PxvrcpZJwGs) 동영상 73 | * redirect와 foward 방식의 차이 74 | * 개발할 때 고려할 사항 75 | 76 | ### [JSTL와 EL(expression language)](https://youtu.be/Sk9-mfKpvtQ) 동영상 77 | * jstl 라이브러리 다운로드 및 프로젝트에 추가 78 | * jstl 활용해 if/else 구현 79 | * expression language 활용해 스크립틀릿 코드 제거 80 | 81 | # 데이터베이스 연동 82 | ### [JDBC 설치 및 학습](https://youtu.be/uI97GlQKcg0) 동영상 83 | * 데이터베이스가 하는 역할 84 | * 데이터베이스에 데이터베이스와 테이블 생성 85 | * JDBC 드라이버 설치 86 | * 사용자 데이터 추가(insert) 87 | * Junit 단위 테스트를 통한 JDBC 학습 88 | 89 | ### [DB에서 데이터 조회](https://youtu.be/ljoePTUVFJU) 동영상 90 | * 데이터 조회 기능 추가(select) 91 | * Database를 사용하던 코드가 UserDAO를 사용하도록 구현함. 92 | 93 | ### [DB 테스트](https://youtu.be/aDUdROOSznE) 동영상 94 | * 사용하지 않는 Database 클래스 제거 95 | * 반복 테스트가 가능하도록 사용자 제거 기능 추가 96 | * DB 반복 테스트가 가능하도록 구현함 97 | 98 | # 회원가입 기능 구현 지속 및 코드 리팩토링 99 | ### [개인 정보 수정 폼 개발](https://youtu.be/souamHDBev8) 동영상 100 | JSP에서 자바 빈 데이터를 가져올 때 규칙이 있다. 이 규칙을 이해하고 사용해야 한다. 소스 코드를 통해 MVC 각각의 역할을 이해하는 것은 MVC 패턴을 이해하는데 많은 도움이 된다. 최근의 거의 모든 웹 프레임워크가 MVC 패턴을 기반으로 하고 있기 때문에 MVC 패턴에 대해서는 반드시 이해하고 넘어가자. 101 | * JSP에서 자바 코드(스크립틀릿)을 사용하지 않도록 구현하는 방법 102 | * hidden input type 사용 103 | * 자바빈 규약에 따라 자바 객체에서 데이터 추출 104 | * 소스 코드를 통해 MVC 각각의 역할을 설명 105 | 106 | ### [개인 정보 수정 개발 완료](https://youtu.be/jC_i7OAtLfk) 동영상 107 | * 개인 정보 수정을 위한 DAO 개발 완료 108 | 109 | ### [JDBC 리소스 반환](https://youtu.be/ideyEk8kZNQ) 동영상 110 | JDBC를 활용해 프로그래밍을 하는 경우 Connection, PreparedStatement, ResultSet 자원을 사용한 후 반드시 반환해야 한다. 반환은 각 인스턴스의 close() 를 사용해 가능하다. 자바 기반 웹 애플리케이션을 개발할 때 웹 서버가 다운되는 원인의 가장 큰 부분이 DB와의 Connection 자원을 close()하지 않아서 발생했다. 지금은 많이 개선된 상태이다. 111 | * JDBC의 Connection, PreparedStatement, ResultSet의 자원을 반화하도록 리팩토링 112 | 113 | ### [회원가입 화면과 개인정보 수정 화면의 중복 제거](https://youtu.be/k7Rh60ieaSI) 동영상 114 | 회원가입과 개인정보 수정 화면을 담당하는 jsp를 보면 많은 중복이 발생한다. 일반적으로 데이터를 생성하고 수정하는 화면은 많은 중복이 발생하기 때문에 jsp 하나로 생성과 수정 화면을 모두 담당하도록 구현해 중복을 제거한다. 중복은 어느 곳에서나 해악이므로 jsp에서 최대한 중복 코드를 만들지 않도록 해야한다. 115 | * 회원 가입을 위한 Servlet 추가 116 | * form.jsp와 update_form.jsp 중복 제거해 하나의 파일로 통일 117 | * c:set 태그 사용 118 | 119 | ### [개인정보 수정에 대한 보안 처리](https://youtu.be/kO06A7nWhS4) 동영상 120 | 개인 정보 수정, 게시글 삭제와 같은 기능을 개발할 때는 보안에 특히 주의해야 한다. 서버측 프로그래밍은 보안적으로 문제가 발생할 경우 관리하고 있는 모든 데이터에 접근할 수 있기 때문에 보안적으로 문제가 없는지 고려해야 개발해야 한다. 기본적인 기능이 정상적으로 동작하도록 구현하는 것도 중요하지만 안전한 소프트웨어가 되도록 구현하는데도 많은 시간과 노력을 투자해야 한다. 121 | * UpdateUserServlet에서 로그인 사용자만 접근하도록 처리 122 | * 자신의 계정 정보만 수정할 수 있도록 보안 처리 123 | * 중복 코드에 대한 리팩토링 작업 124 | 125 | ### [gson 라이브러리를 활용한 api 개발](https://youtu.be/1yhYpcWMbIk) 동영상 126 | json 또는 xml 데이터 표준으로 개발하는 웹 애플리케이션이 점차 많아지고 있다. 모바일의 등장과 ajax를 기반으로 한 single page appliction 개발이 주요 흐름으로 자리 잡으면서 좋은 api를 개발하는 것에 대한 관심도는 높아지고 있다. api를 개발하기 위해 자바 객체를 json으로, json을 자바 객체로 변환하는 작업이 많이 필요하다. 자바 진영 라이브러리 중에 gson 또는 jackson이 많이 사용된다. 127 | * gson 라이브러리 추가 128 | * gson 라이브러리를 활용해 json 데이터로 변환 129 | * json에 대한 content type 설정 130 | 131 | ### [리팩토링 이슈 찾기, 등록, 관리](https://youtu.be/Umn45aYUukM) 동영상 132 | 팀으로 협업을 하려면 현재 진행되고 있는 작업 목록을 관리해야 한다. 소프트웨어 개발에서 각 작업을 이슈라고 하며, 이슈 목록을 관리하기 위한 도구 중의 하나가 이슈 관리 시스템이다. github은 이슈 관리 시스템 중에서도 정말 필요한 기능만을 가지고 있는 가벼운 도구 중의 하나이다. 프로젝트 진행에 있어 무엇인가 시간을 투자할 필요가 있는 모든 작업은 이슈 관리 시스템에 관리하고, 목표를 정하고, 일정 관리 등을 해야 효율적인 프로젝트 진행이 가능하다. 이슈만 잘 관리하고 현재 상태를 지속적으로 업데이트만 잘 하더라도 프로젝트의 성공 가능성은 높아진다. 133 | * 리팩토링할 요소들 찾기 134 | * github에 이슈 등록 135 | * 마일스톤 관리 및 이슈 관리 136 | 137 | # validator 적용 및 maven 빌드 도구 138 | ### [validator 라이브러리 설치, 적용, 테스트](https://youtu.be/ZnbbqH5SOvM) 동영상 139 | 데이터 관리 측면에서 사용자가 입력한 데이터가 유효한(valid) 데이터인지를 판단하는 것은 정말 중요하다. 유효하지 않은 데이터가 DB에 추가될 경우 예외 상황이 발생하기 때문에 로직 구현이 힘들어 지고 소스 코드 또한 복잡해 진다. 특히 서버측에서는 데이터에 대한 유효성을 철저하게 검증해야 한다. 하지만 이 작업이 상당히 귀찮은 작업이고, 중복 코드가 많으며, 코드량도 많아진다. 따라서 이 같은 기능을 지원하는 라이브러리가 있는지 찾아서 적용해 보고, 없을 경우 자체적으로 만들어 보는 것도 좋겠다. 자바 진영에서는 유효성을 체크하기 위한 validation api 표준이 있으며, 이 표준 구현체 중에 hibernate validation을 가장 많이 사용하고 있다. 140 | * 외부 라이브러리를 찾아 적용하는 과정 141 | * java validator 라이브러리 적용 142 | * 단위 테스트를 활용해 java validator 학습 143 | 144 | ### [회원가입 기능에 validator 적용](https://youtu.be/JMQIaYIp5-g) 동영상 145 | * 회원가입 Servlet에 validator 적용 146 | 147 | ### [빌드 도구 설명, 메이븐 프로젝트 생성, 의존성 관리](https://youtu.be/Eg1Ebl_KNFg) 동영상 148 | * ant/maven/gradle 빌드 도구에 대한 초간단 설명 149 | * eclipse에서 메이븐 디렉토리 구조의 프로젝트 생성 150 | * junit 의존성 추가 * 메이븐 의존성 전이 설명 151 | 152 | ### [부모 pom, 기본 디렉토리 설정, 메이븐 기본 명령어](https://youtu.be/A8h1y-qXCbU) 동영상 153 | * effective pom 탭을 통해 메이븐 부모 pom 설명 154 | * 메이븐 기본 명령어인 compile/test/package 설명 155 | * eclipse에서 메이븐 명령어 실행 156 | 157 | ### [메이븐 phase, goal, 플러그인 설정 재정의](https://youtu.be/58yiJQU0xEY) 동영상 158 | * 메이븐의 phase와 goal 관계 설명 159 | * compiler 플러그인과 eclipse 플러그인 재정의 및 빌드 160 | * eclipse에서 효율적인 메이븐 goal 실행 방법 161 | 162 | ### [slipp 프로젝트에 메이븐 적용](https://youtu.be/ovpVzUaQtSM) 동영상 163 | 지금까지 개발하고 있는 프로젝트의 디렉토리 구조를 변경하지 않으면서 메이븐을 적용할 수 있다. 메이븐을 적용할 경우 지금까지 github에 공유하던 많은 소스 코드를 공유하지 않아도 된다. 특히 eclipse 관련 설정과 jar 라이브러리를 공유하지 않아도 되는 것은 큰 장점이다. 164 | * slipp 프로젝트에 메이븐 적용하는 과정 진행 165 | * github에서 jar 파일을 버전 관리하지 않도록 설정함. 166 | 167 | # 자바 웹 애플리케이션 배포 및 logging 168 | ### [서블릿 컨테이너 및 Tomcat 디렉토리 구조 설명](https://youtu.be/WdBAto3IQOg) 동영상 169 | Embedded Tomcat을 사용하다보면 Tomcat 구조에 대해 익힐 기회가 많지 않다. 그냥 사용하면 되니까. 하지만 개발 서버, 실 서버로 넘어가면 이야기가 달라진다. 이 시점부터는 Tomcat 서버에 직접 접속해 시작하고 디버깅 로그 파악해야 하기 때문에 Tomcat에 대한 기본적인 이해는 하고 있어야 한다. 170 | * 서블릿 컨테이너 설명 171 | * 터미널에서 Tomcat 시작, 종료 172 | * Tomcat 디렉토리 구조 설명 173 | * tail 명령어를 활용한 로그 확인 174 | 175 | ### [war 파일 빌드, 설정, Tomcat 서버 배포](https://youtu.be/K84mSiC_q6I) 동영상 176 | war는 web archive를 줄인 말이다. 웹 애플리케이션은 war 파일로 만들어 배포할 수 있다. war 파일은 메이븐 빌드 도구의 package 명령을 실행해 가능하다. 배포 형태를 디렉토리를 통째로 복사하거나 war 파일로 배포하는 두가지 형태가 있다. 환경에 따라 두 가지 방법 중 하나를 사용하면 된다. 177 | * war 파일 생성을 위한 설정 178 | * war 파일 생성 179 | * war 파일을 Tomcat 서버에 배포 180 | 181 | ### [tomcat context 설정](https://youtu.be/MkMyNEaCDeY) 동영상 182 | Tomcat은 webapps 디렉토리에 웹 애플리케이션을 바로 배포할 수 있다. 또 다른 방법은 conf/server.xml 설정을 추가해 임의의 디렉토리에 있는 웹 애플리케이션을 배포하는 것이 가능하다. 현장에서는 server.xml의 설정을 변경해 배포하는 방법을 많이 사용한다. 183 | * 메이븐 finalName 설정 184 | * server.xml에서 tomcat context 설정 185 | * 개발 서버에서 웹 애플리케이션 배포시 디렉토리 구조 186 | 187 | ### [개발 서버에 소스 코드 배포 과정에 대한 자동화](https://youtu.be/X6UgSdjvrBU) 동영상 188 | 소스 코드를 빌드/배포하는 과정에서 수 많은 반복 작업이 발생한다. 반복 작업은 사람의 실수를 유발하고 이는 대규모 장애로 이어진다. 따라서 반복 작업은 컴퓨터를 통해 자동화하는 것이 가장 안전하다. 그리고 개발자는 소중한 존재이므로 이런 하찮은 반복 작업에 시간을 쓰지 않도록 노력해야 한다. 틈틈히 shell script 만드는 연습을 하면 많은 반복 작업을 줄일 수 있다. 189 | * 배포 과정에서 발생하는 반복 과정에 대한 설명 190 | * sh 파일을 이용해 배포 과정을 자동화 191 | 192 | ### [logging framework 개념 및 기본 설정](https://youtu.be/TcKEGh7KShI) 동영상 193 | System.out.println을 사용하는 것은 많은 문제점을 가지고 있기 때문에 현장에서는 사용하지 않는다. 현장에서는 logging framework를 사용한다. logging framework은 로그 메시지를 관리하기 위한 다양한 기능을 지원한다. logging framework은 개발자가 반드시 익혀야할 도구 중의 하나이니 이번 기획에 확실히 익혔으면 한다. 194 | * logging framework이 필요한 이유 195 | * logback framework 설정 196 | 197 | ### [logging framework 추가 설정 및 eclipse 설정](https://youtu.be/040Y3MBNnyw) 동영상 198 | logging framework을 사용할 때 성능 측면에서 고려할 부분이 있다. 사용자가 많지 않은 경우에는 큰 문제 없지만 만약지면 이런 작은 차이가 크다. 시작부터 좋은 습관을 들여놓으면 좋겠다. 개발자가 항상 가져야 하는 습관은 단순, 반복적인 작업을 제거, 중복 코드를 제거하는 습관을 들여야 한다. eclipse와 같은 통합 개발 도구를 활용해 많은 작업을 효율화할 수 있다. 자신이 낭비라고 생각하는 작업이 있다면 해결 방법이 없는지 찾는 연습을 해보자. 199 | * logging level 설명 200 | * 패키지별 logging framework 설정 201 | * debug 메시지 구현시 주의할 점 공유 202 | * logging을 위해 반복적으로 추가되는 설정을 eclipse template으로 해결 203 | * eclipse formatter 설정 204 | 205 | > 아직 빌드가 무엇인지, 빌드 도구가 무엇이며, 왜 필요한 것인지 감이 잘 오지 않을 것이다. 빌드 도구의 필요성을 제대로 느껴보려면 개발 서버에 지금까지 만든 서비스를 배포해 보는 경험을 해야 한다. 그 전이라도 라이브러리 관리만으로도 많은 도움을 받을 것이다. 빌드 도구 중의 하나인 메이븐에 대해 다루고 있다. 이 동영상만으로 이해하는데 어려움이 있기 때문에 동영상에서 추천하는 자료와 같이 학습하면 좋겠다. 206 | 207 | # DAO 리팩토링 208 | > JDBC 기반으로 프로그래밍하다보면 정말 많은 중복이 발생한다. 이 중복을 어떻게 제거할 것인지에 대한 연습은 공통 라이브러리 코드를 어떻게 만드는 것이 좋을 것인지에 대한 학습을 하는데 큰 도움이 된다. JDBC 중복을 제거하는 과정을 보면 리팩토링을 어떻게 하는 것이 유연한 코드를 만들고, 깔끔한 코드를 만드는 것인지에 대한 경험을 할 수 있다. 이 과정에는 Template Method 패턴, Callback Interface등 디자인 패턴과 관련한 내용도 등장해야 한다. 디자인 패턴은 수 많은 선배 개발자들이 문제에 부딪혔을 때 좀 더 깔끔하고 유연한 코드를 만들기 위해 고민했던 흔적을 느껴볼 수 있다. 여력이 된다면 동영상을 본 후에 혼자 힘으로 리팩토링하는 연습을 해보면 좋겠다. 209 | 210 | > 자바와 리팩토링에 익숙하지 않을 경우 과정이 다소 어려울 수 있다. 앞의 단계에서 학습을 마무리한 후 일정 경험을 쌓은 후 다시 도전하는 것도 한 가지 방법이다. 211 | 212 | ### [DAO 리팩토링 1 - insert, update, delete 중복 제거](https://youtu.be/ylrMBeakVnk) 동영상 213 | * DAO 중복 코드에 대한 이슈 제기 214 | * 라이브러리 코드와 개발자가 구현해야 하는 코드 분리 215 | * abstract 키워드를 활용해 추상 클래스 구현 216 | * 익명 클래스 사용 217 | * insert, update, delete 문에 대한 중복 제거 218 | 219 | ### [DAO 리팩토링 2 - select 문 중복 제거](https://youtu.be/zfXAZkqPH44) 동영상 220 | * select 쿼리에 대한 중복 코드 제거 221 | * getConnection 메서드 중복 제거 및 테스트 코드 수정 222 | * Template Method 패턴 223 | 224 | ### [DAO 리팩토링 3 - JdbcTemplate과 SelectJdbcTemplate 통합](https://youtu.be/yEHUB97B62I) 동영상 225 | * Template Method 패턴을 활용해 JdbcTemplate과 SelectJdbcTemplate 통합 226 | * Template Method 패턴을 활용할 때의 문제점 227 | * 각 Method를 interface로 분리해 JdbcTemplate과 SelectJdbcTemplate 통합 228 | 229 | ### [DAO 리팩토링 4 - 라이브러리 리팩토링 및 목록 기능 추가](https://youtu.be/nkepkHJi7e8) 동영상 230 | * 자바 Generic을 활용해 캐스팅을 하지 않도록 라이브러리 구현 231 | * 가변 인자를 활용해 개발자 편의성 개선 232 | * 여러 건의 데이터를 조회할 수 있는 기능 추가 233 | * 라이브러리 코드에서 중복을 제거하는 과정에 대한 설명 234 | 235 | ### [DAO 리팩토링 5 - SQLException을 DataAccessException으로 래핑](https://youtu.be/lFTyw7Uipyo) 동영상 236 | 자바 언어가 등장하면서 장점 중의 하나가 Exception 클래스의 도입이었다. 그런데 Checked Exception이 남용되면서 소스 코드가 지저분해지는 경향이 생기면서 최근에는 Checked Exception의 사용을 최소화하자는 방향으로 흐름이 바뀌였다. 하지만 기존 API에는 수 많은 Checked Exception이 있다. 이 Checked Exception을 그대로 사용하는 것이 아니라 UnChecked Exception(Runtime Exception)으로 래핑해서 사용하는 것이 더 깔끔한 코드를 유지하는데 도움이 된다. 237 | * Checked Exception과 Unchecked Exception 238 | * SQLException을 Unchecked Exception으로 변환 처리 239 | 240 | ### [DAO 리팩토링 6 - 람다 표현식을 사용하도록 리팩토링](https://youtu.be/0ax9jxfW9x4) 동영상 241 | 자바 8에 추가된 람다 표현식은 자바 코드를 좀 더 깔끔하게 구현할 수 있도록 지원한다. 이미 수 많은 언어에서 지원하고 있는 람다 표현식이 늦게 추가된 경향이 있다. 람다 표현식은 함수형 프로그래밍에서 널리 사용되고 있는 것으로 익혀 놓으면 추후 다른 언어 기반으로 구현하는데도 도움이 될 것으로 생각한다. 242 | * RowMapper에 FunctionalInterface 어노테이션 설정 243 | * RowMapper를 사용할 때 람다 표현식을 사용하도록 리팩토링 -------------------------------------------------------------------------------- /webapp/stylesheets/docs.css: -------------------------------------------------------------------------------- 1 | /* Add additional stylesheets below 2 | -------------------------------------------------- */ 3 | /* 4 | Bootstrap's documentation styles 5 | Special styles for presenting Bootstrap's documentation and examples 6 | */ 7 | 8 | 9 | 10 | /* Body and structure 11 | -------------------------------------------------- */ 12 | 13 | body { 14 | position: relative; 15 | padding-top: 40px; 16 | } 17 | 18 | /* Code in headings */ 19 | h3 code { 20 | font-size: 14px; 21 | font-weight: normal; 22 | } 23 | 24 | 25 | 26 | /* Tweak navbar brand link to be super sleek 27 | -------------------------------------------------- */ 28 | 29 | body > .navbar { 30 | font-size: 13px; 31 | } 32 | 33 | /* Change the docs' brand */ 34 | body > .navbar .brand { 35 | padding-right: 0; 36 | padding-left: 0; 37 | margin-left: 20px; 38 | float: right; 39 | font-weight: bold; 40 | color: #000; 41 | text-shadow: 0 1px 0 rgba(255,255,255,.1), 0 0 30px rgba(255,255,255,.125); 42 | -webkit-transition: all .2s linear; 43 | -moz-transition: all .2s linear; 44 | transition: all .2s linear; 45 | } 46 | body > .navbar .brand:hover { 47 | text-decoration: none; 48 | text-shadow: 0 1px 0 rgba(255,255,255,.1), 0 0 30px rgba(255,255,255,.4); 49 | } 50 | 51 | 52 | /* Sections 53 | -------------------------------------------------- */ 54 | 55 | /* padding for in-page bookmarks and fixed navbar */ 56 | section { 57 | padding-top: 30px; 58 | } 59 | section > .page-header, 60 | section > .lead { 61 | color: #5a5a5a; 62 | } 63 | section > ul li { 64 | margin-bottom: 5px; 65 | } 66 | 67 | /* Separators (hr) */ 68 | .bs-docs-separator { 69 | margin: 40px 0 39px; 70 | } 71 | 72 | /* Faded out hr */ 73 | hr.soften { 74 | height: 1px; 75 | margin: 70px 0; 76 | background-image: -webkit-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 77 | background-image: -moz-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 78 | background-image: -ms-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 79 | background-image: -o-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 80 | border: 0; 81 | } 82 | 83 | 84 | 85 | /* Jumbotrons 86 | -------------------------------------------------- */ 87 | 88 | /* Base class 89 | ------------------------- */ 90 | .jumbotron { 91 | position: relative; 92 | padding: 40px 0; 93 | color: #fff; 94 | text-align: center; 95 | text-shadow: 0 1px 3px rgba(0,0,0,.4), 0 0 30px rgba(0,0,0,.075); 96 | background: #020031; /* Old browsers */ 97 | background: -moz-linear-gradient(45deg, #020031 0%, #6d3353 100%); /* FF3.6+ */ 98 | background: -webkit-gradient(linear, left bottom, right top, color-stop(0%,#020031), color-stop(100%,#6d3353)); /* Chrome,Safari4+ */ 99 | background: -webkit-linear-gradient(45deg, #020031 0%,#6d3353 100%); /* Chrome10+,Safari5.1+ */ 100 | background: -o-linear-gradient(45deg, #020031 0%,#6d3353 100%); /* Opera 11.10+ */ 101 | background: -ms-linear-gradient(45deg, #020031 0%,#6d3353 100%); /* IE10+ */ 102 | background: linear-gradient(45deg, #020031 0%,#6d3353 100%); /* W3C */ 103 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#020031', endColorstr='#6d3353',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */ 104 | -webkit-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); 105 | -moz-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); 106 | box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); 107 | } 108 | .jumbotron h1 { 109 | font-size: 80px; 110 | font-weight: bold; 111 | letter-spacing: -1px; 112 | line-height: 1; 113 | } 114 | .jumbotron p { 115 | font-size: 24px; 116 | font-weight: 300; 117 | line-height: 1.25; 118 | margin-bottom: 30px; 119 | } 120 | 121 | /* Link styles (used on .masthead-links as well) */ 122 | .jumbotron a { 123 | color: #fff; 124 | color: rgba(255,255,255,.5); 125 | -webkit-transition: all .2s ease-in-out; 126 | -moz-transition: all .2s ease-in-out; 127 | transition: all .2s ease-in-out; 128 | } 129 | .jumbotron a:hover { 130 | color: #fff; 131 | text-shadow: 0 0 10px rgba(255,255,255,.25); 132 | } 133 | 134 | /* Download button */ 135 | .masthead .btn { 136 | padding: 19px 24px; 137 | font-size: 24px; 138 | font-weight: 200; 139 | color: #fff; /* redeclare to override the `.jumbotron a` */ 140 | border: 0; 141 | -webkit-border-radius: 6px; 142 | -moz-border-radius: 6px; 143 | border-radius: 6px; 144 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 145 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 146 | box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 147 | -webkit-transition: none; 148 | -moz-transition: none; 149 | transition: none; 150 | } 151 | .masthead .btn:hover { 152 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 153 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 154 | box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 155 | } 156 | .masthead .btn:active { 157 | -webkit-box-shadow: inset 0 2px 4px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.1); 158 | -moz-box-shadow: inset 0 2px 4px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.1); 159 | box-shadow: inset 0 2px 4px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.1); 160 | } 161 | 162 | 163 | /* Pattern overlay 164 | ------------------------- */ 165 | .jumbotron .container { 166 | position: relative; 167 | z-index: 2; 168 | } 169 | .jumbotron:after { 170 | content: ''; 171 | display: block; 172 | position: absolute; 173 | top: 0; 174 | right: 0; 175 | bottom: 0; 176 | left: 0; 177 | background: url(../img/bs-docs-masthead-pattern.png) repeat center center; 178 | opacity: .4; 179 | } 180 | 181 | /* Masthead (docs home) 182 | ------------------------- */ 183 | .masthead { 184 | padding: 70px 0 80px; 185 | margin-bottom: 0; 186 | color: #fff; 187 | } 188 | .masthead h1 { 189 | font-size: 120px; 190 | line-height: 1; 191 | letter-spacing: -2px; 192 | } 193 | .masthead p { 194 | font-size: 40px; 195 | font-weight: 200; 196 | line-height: 1.25; 197 | } 198 | 199 | /* Textual links in masthead */ 200 | .masthead-links { 201 | margin: 0; 202 | list-style: none; 203 | } 204 | .masthead-links li { 205 | display: inline; 206 | padding: 0 10px; 207 | color: rgba(255,255,255,.25); 208 | } 209 | 210 | /* Social proof buttons from GitHub & Twitter */ 211 | .bs-docs-social { 212 | padding: 15px 0; 213 | text-align: center; 214 | background-color: #f5f5f5; 215 | border-top: 1px solid #fff; 216 | border-bottom: 1px solid #ddd; 217 | } 218 | 219 | /* Quick links on Home */ 220 | .bs-docs-social-buttons { 221 | margin-left: 0; 222 | margin-bottom: 0; 223 | padding-left: 0; 224 | list-style: none; 225 | } 226 | .bs-docs-social-buttons li { 227 | display: inline-block; 228 | padding: 5px 8px; 229 | line-height: 1; 230 | *display: inline; 231 | *zoom: 1; 232 | } 233 | 234 | /* Subhead (other pages) 235 | ------------------------- */ 236 | .subhead { 237 | text-align: left; 238 | border-bottom: 1px solid #ddd; 239 | } 240 | .subhead h1 { 241 | font-size: 60px; 242 | } 243 | .subhead p { 244 | margin-bottom: 20px; 245 | } 246 | .subhead .navbar { 247 | display: none; 248 | } 249 | 250 | 251 | 252 | /* Marketing section of Overview 253 | -------------------------------------------------- */ 254 | 255 | .marketing { 256 | text-align: center; 257 | color: #5a5a5a; 258 | } 259 | .marketing h1 { 260 | margin: 60px 0 10px; 261 | font-size: 60px; 262 | font-weight: 200; 263 | line-height: 1; 264 | letter-spacing: -1px; 265 | } 266 | .marketing h2 { 267 | font-weight: 200; 268 | margin-bottom: 5px; 269 | } 270 | .marketing p { 271 | font-size: 16px; 272 | line-height: 1.5; 273 | } 274 | .marketing .marketing-byline { 275 | margin-bottom: 40px; 276 | font-size: 20px; 277 | font-weight: 300; 278 | line-height: 1.25; 279 | color: #999; 280 | } 281 | .marketing img { 282 | display: block; 283 | margin: 0 auto 30px; 284 | } 285 | 286 | 287 | 288 | /* Footer 289 | -------------------------------------------------- */ 290 | 291 | .footer { 292 | padding: 70px 0; 293 | margin-top: 70px; 294 | border-top: 1px solid #e5e5e5; 295 | background-color: #f5f5f5; 296 | } 297 | .footer p { 298 | margin-bottom: 0; 299 | color: #777; 300 | } 301 | .footer-links { 302 | margin: 10px 0; 303 | } 304 | .footer-links li { 305 | display: inline; 306 | padding: 0 2px; 307 | } 308 | .footer-links li:first-child { 309 | padding-left: 0; 310 | } 311 | 312 | 313 | 314 | /* Special grid styles 315 | -------------------------------------------------- */ 316 | 317 | .show-grid { 318 | margin-top: 10px; 319 | margin-bottom: 20px; 320 | } 321 | .show-grid [class*="span"] { 322 | background-color: #eee; 323 | text-align: center; 324 | -webkit-border-radius: 3px; 325 | -moz-border-radius: 3px; 326 | border-radius: 3px; 327 | min-height: 40px; 328 | line-height: 40px; 329 | } 330 | .show-grid:hover [class*="span"] { 331 | background: #ddd; 332 | } 333 | .show-grid .show-grid { 334 | margin-top: 0; 335 | margin-bottom: 0; 336 | } 337 | .show-grid .show-grid [class*="span"] { 338 | background-color: #ccc; 339 | } 340 | 341 | 342 | 343 | /* Mini layout previews 344 | -------------------------------------------------- */ 345 | .mini-layout { 346 | border: 1px solid #ddd; 347 | -webkit-border-radius: 6px; 348 | -moz-border-radius: 6px; 349 | border-radius: 6px; 350 | -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.075); 351 | -moz-box-shadow: 0 1px 2px rgba(0,0,0,.075); 352 | box-shadow: 0 1px 2px rgba(0,0,0,.075); 353 | } 354 | .mini-layout, 355 | .mini-layout .mini-layout-body, 356 | .mini-layout.fluid .mini-layout-sidebar { 357 | height: 300px; 358 | } 359 | .mini-layout { 360 | margin-bottom: 20px; 361 | padding: 9px; 362 | } 363 | .mini-layout div { 364 | -webkit-border-radius: 3px; 365 | -moz-border-radius: 3px; 366 | border-radius: 3px; 367 | } 368 | .mini-layout .mini-layout-body { 369 | background-color: #dceaf4; 370 | margin: 0 auto; 371 | width: 70%; 372 | } 373 | .mini-layout.fluid .mini-layout-sidebar, 374 | .mini-layout.fluid .mini-layout-header, 375 | .mini-layout.fluid .mini-layout-body { 376 | float: left; 377 | } 378 | .mini-layout.fluid .mini-layout-sidebar { 379 | background-color: #bbd8e9; 380 | width: 20%; 381 | } 382 | .mini-layout.fluid .mini-layout-body { 383 | width: 77.5%; 384 | margin-left: 2.5%; 385 | } 386 | 387 | 388 | 389 | /* Download page 390 | -------------------------------------------------- */ 391 | 392 | .download .page-header { 393 | margin-top: 36px; 394 | } 395 | .page-header .toggle-all { 396 | margin-top: 5px; 397 | } 398 | 399 | /* Space out h3s when following a section */ 400 | .download h3 { 401 | margin-bottom: 5px; 402 | } 403 | .download-builder input + h3, 404 | .download-builder .checkbox + h3 { 405 | margin-top: 9px; 406 | } 407 | 408 | /* Fields for variables */ 409 | .download-builder input[type=text] { 410 | margin-bottom: 9px; 411 | font-family: Menlo, Monaco, "Courier New", monospace; 412 | font-size: 12px; 413 | color: #d14; 414 | } 415 | .download-builder input[type=text]:focus { 416 | background-color: #fff; 417 | } 418 | 419 | /* Custom, larger checkbox labels */ 420 | .download .checkbox { 421 | padding: 6px 10px 6px 25px; 422 | font-size: 13px; 423 | line-height: 18px; 424 | color: #555; 425 | background-color: #f9f9f9; 426 | -webkit-border-radius: 3px; 427 | -moz-border-radius: 3px; 428 | border-radius: 3px; 429 | cursor: pointer; 430 | } 431 | .download .checkbox:hover { 432 | color: #333; 433 | background-color: #f5f5f5; 434 | } 435 | .download .checkbox small { 436 | font-size: 12px; 437 | color: #777; 438 | } 439 | 440 | /* Variables section */ 441 | #variables label { 442 | margin-bottom: 0; 443 | } 444 | 445 | /* Giant download button */ 446 | .download-btn { 447 | margin: 36px 0 108px; 448 | } 449 | #download p, 450 | #download h4 { 451 | max-width: 50%; 452 | margin: 0 auto; 453 | color: #999; 454 | text-align: center; 455 | } 456 | #download h4 { 457 | margin-bottom: 0; 458 | } 459 | #download p { 460 | margin-bottom: 18px; 461 | } 462 | .download-btn .btn { 463 | display: block; 464 | width: auto; 465 | padding: 19px 24px; 466 | margin-bottom: 27px; 467 | font-size: 30px; 468 | line-height: 1; 469 | text-align: center; 470 | -webkit-border-radius: 6px; 471 | -moz-border-radius: 6px; 472 | border-radius: 6px; 473 | } 474 | 475 | 476 | 477 | /* Misc 478 | -------------------------------------------------- */ 479 | 480 | /* Make tables spaced out a bit more */ 481 | h2 + table, 482 | h3 + table, 483 | h4 + table, 484 | h2 + .row { 485 | margin-top: 5px; 486 | } 487 | 488 | /* Example sites showcase */ 489 | .example-sites { 490 | xmargin-left: 20px; 491 | } 492 | .example-sites img { 493 | max-width: 100%; 494 | margin: 0 auto; 495 | } 496 | 497 | .scrollspy-example { 498 | height: 200px; 499 | overflow: auto; 500 | position: relative; 501 | } 502 | 503 | 504 | /* Fake the :focus state to demo it */ 505 | .focused { 506 | border-color: rgba(82,168,236,.8); 507 | -webkit-box-shadow: inset 0 1px 3px rgba(0,0,0,.1), 0 0 8px rgba(82,168,236,.6); 508 | -moz-box-shadow: inset 0 1px 3px rgba(0,0,0,.1), 0 0 8px rgba(82,168,236,.6); 509 | box-shadow: inset 0 1px 3px rgba(0,0,0,.1), 0 0 8px rgba(82,168,236,.6); 510 | outline: 0; 511 | } 512 | 513 | /* For input sizes, make them display block */ 514 | .docs-input-sizes select, 515 | .docs-input-sizes input[type=text] { 516 | display: block; 517 | margin-bottom: 9px; 518 | } 519 | 520 | /* Icons 521 | ------------------------- */ 522 | .the-icons { 523 | margin-left: 0; 524 | list-style: none; 525 | } 526 | .the-icons li { 527 | float: left; 528 | width: 25%; 529 | line-height: 25px; 530 | } 531 | .the-icons i:hover { 532 | background-color: rgba(255,0,0,.25); 533 | } 534 | 535 | /* Example page 536 | ------------------------- */ 537 | .bootstrap-examples p { 538 | font-size: 13px; 539 | line-height: 18px; 540 | } 541 | .bootstrap-examples .thumbnail { 542 | margin-bottom: 9px; 543 | background-color: #fff; 544 | } 545 | 546 | 547 | 548 | /* Bootstrap code examples 549 | -------------------------------------------------- */ 550 | 551 | /* Base class */ 552 | .bs-docs-example { 553 | position: relative; 554 | margin: 15px 0; 555 | padding: 39px 19px 14px; 556 | *padding-top: 19px; 557 | background-color: #fff; 558 | border: 1px solid #ddd; 559 | -webkit-border-radius: 4px; 560 | -moz-border-radius: 4px; 561 | border-radius: 4px; 562 | } 563 | 564 | /* Echo out a label for the example */ 565 | .bs-docs-example:after { 566 | content: "Example"; 567 | position: absolute; 568 | top: -1px; 569 | left: -1px; 570 | padding: 3px 7px; 571 | font-size: 12px; 572 | font-weight: bold; 573 | background-color: #f5f5f5; 574 | border: 1px solid #ddd; 575 | color: #9da0a4; 576 | -webkit-border-radius: 4px 0 4px 0; 577 | -moz-border-radius: 4px 0 4px 0; 578 | border-radius: 4px 0 4px 0; 579 | } 580 | 581 | /* Remove spacing between an example and it's code */ 582 | .bs-docs-example + .prettyprint { 583 | margin-top: -20px; 584 | padding-top: 15px; 585 | } 586 | 587 | /* Tweak examples 588 | ------------------------- */ 589 | .bs-docs-example > p:last-child { 590 | margin-bottom: 0; 591 | } 592 | .bs-docs-example .table, 593 | .bs-docs-example .progress, 594 | .bs-docs-example .well, 595 | .bs-docs-example .alert, 596 | .bs-docs-example .hero-unit, 597 | .bs-docs-example .pagination, 598 | .bs-docs-example .navbar, 599 | .bs-docs-example > .nav, 600 | .bs-docs-example blockquote { 601 | margin-bottom: 5px; 602 | } 603 | .bs-docs-example .pagination { 604 | margin-top: 0; 605 | } 606 | .bs-navbar-top-example, 607 | .bs-navbar-bottom-example { 608 | z-index: 1; 609 | padding: 0; 610 | height: 90px; 611 | overflow: hidden; /* cut the drop shadows off */ 612 | } 613 | .bs-navbar-top-example .navbar-fixed-top, 614 | .bs-navbar-bottom-example .navbar-fixed-bottom { 615 | margin-left: 0; 616 | margin-right: 0; 617 | } 618 | .bs-navbar-top-example { 619 | -webkit-border-radius: 0 0 4px 4px; 620 | -moz-border-radius: 0 0 4px 4px; 621 | border-radius: 0 0 4px 4px; 622 | } 623 | .bs-navbar-top-example:after { 624 | top: auto; 625 | bottom: -1px; 626 | -webkit-border-radius: 0 4px 0 4px; 627 | -moz-border-radius: 0 4px 0 4px; 628 | border-radius: 0 4px 0 4px; 629 | } 630 | .bs-navbar-bottom-example { 631 | -webkit-border-radius: 4px 4px 0 0; 632 | -moz-border-radius: 4px 4px 0 0; 633 | border-radius: 4px 4px 0 0; 634 | } 635 | .bs-navbar-bottom-example .navbar { 636 | margin-bottom: 0; 637 | } 638 | form.bs-docs-example { 639 | padding-bottom: 19px; 640 | } 641 | 642 | /* Images */ 643 | .bs-docs-example-images img { 644 | margin: 10px; 645 | display: inline-block; 646 | } 647 | 648 | /* Tooltips */ 649 | .bs-docs-tooltip-examples { 650 | text-align: center; 651 | margin: 0 0 10px; 652 | list-style: none; 653 | } 654 | .bs-docs-tooltip-examples li { 655 | display: inline; 656 | padding: 0 10px; 657 | } 658 | 659 | /* Popovers */ 660 | .bs-docs-example-popover { 661 | padding-bottom: 24px; 662 | background-color: #f9f9f9; 663 | } 664 | .bs-docs-example-popover .popover { 665 | position: relative; 666 | display: block; 667 | float: left; 668 | width: 260px; 669 | margin: 20px; 670 | } 671 | 672 | 673 | 674 | /* Responsive docs 675 | -------------------------------------------------- */ 676 | 677 | /* Utility classes table 678 | ------------------------- */ 679 | .responsive-utilities th small { 680 | display: block; 681 | font-weight: normal; 682 | color: #999; 683 | } 684 | .responsive-utilities tbody th { 685 | font-weight: normal; 686 | } 687 | .responsive-utilities td { 688 | text-align: center; 689 | } 690 | .responsive-utilities td.is-visible { 691 | color: #468847; 692 | background-color: #dff0d8 !important; 693 | } 694 | .responsive-utilities td.is-hidden { 695 | color: #ccc; 696 | background-color: #f9f9f9 !important; 697 | } 698 | 699 | /* Responsive tests 700 | ------------------------- */ 701 | .responsive-utilities-test { 702 | margin-top: 5px; 703 | margin-left: 0; 704 | list-style: none; 705 | overflow: hidden; /* clear floats */ 706 | } 707 | .responsive-utilities-test li { 708 | position: relative; 709 | float: left; 710 | width: 25%; 711 | height: 43px; 712 | font-size: 14px; 713 | font-weight: bold; 714 | line-height: 43px; 715 | color: #999; 716 | text-align: center; 717 | border: 1px solid #ddd; 718 | -webkit-border-radius: 4px; 719 | -moz-border-radius: 4px; 720 | border-radius: 4px; 721 | } 722 | .responsive-utilities-test li + li { 723 | margin-left: 10px; 724 | } 725 | .responsive-utilities-test span { 726 | position: absolute; 727 | top: -1px; 728 | left: -1px; 729 | right: -1px; 730 | bottom: -1px; 731 | -webkit-border-radius: 4px; 732 | -moz-border-radius: 4px; 733 | border-radius: 4px; 734 | } 735 | .responsive-utilities-test span { 736 | color: #468847; 737 | background-color: #dff0d8; 738 | border: 1px solid #d6e9c6; 739 | } 740 | 741 | 742 | 743 | /* Sidenav for Docs 744 | -------------------------------------------------- */ 745 | 746 | .bs-docs-sidenav { 747 | width: 228px; 748 | margin: 30px 0 0; 749 | padding: 0; 750 | background-color: #fff; 751 | -webkit-border-radius: 6px; 752 | -moz-border-radius: 6px; 753 | border-radius: 6px; 754 | -webkit-box-shadow: 0 1px 4px rgba(0,0,0,.065); 755 | -moz-box-shadow: 0 1px 4px rgba(0,0,0,.065); 756 | box-shadow: 0 1px 4px rgba(0,0,0,.065); 757 | } 758 | .bs-docs-sidenav > li > a { 759 | display: block; 760 | width: 190px \9; 761 | margin: 0 0 -1px; 762 | padding: 8px 14px; 763 | border: 1px solid #e5e5e5; 764 | } 765 | .bs-docs-sidenav > li:first-child > a { 766 | -webkit-border-radius: 6px 6px 0 0; 767 | -moz-border-radius: 6px 6px 0 0; 768 | border-radius: 6px 6px 0 0; 769 | } 770 | .bs-docs-sidenav > li:last-child > a { 771 | -webkit-border-radius: 0 0 6px 6px; 772 | -moz-border-radius: 0 0 6px 6px; 773 | border-radius: 0 0 6px 6px; 774 | } 775 | .bs-docs-sidenav > .active > a { 776 | position: relative; 777 | z-index: 2; 778 | padding: 9px 15px; 779 | border: 0; 780 | text-shadow: 0 1px 0 rgba(0,0,0,.15); 781 | -webkit-box-shadow: inset 1px 0 0 rgba(0,0,0,.1), inset -1px 0 0 rgba(0,0,0,.1); 782 | -moz-box-shadow: inset 1px 0 0 rgba(0,0,0,.1), inset -1px 0 0 rgba(0,0,0,.1); 783 | box-shadow: inset 1px 0 0 rgba(0,0,0,.1), inset -1px 0 0 rgba(0,0,0,.1); 784 | } 785 | /* Chevrons */ 786 | .bs-docs-sidenav .icon-chevron-right { 787 | float: right; 788 | margin-top: 2px; 789 | margin-right: -6px; 790 | opacity: .25; 791 | } 792 | .bs-docs-sidenav > li > a:hover { 793 | background-color: #f5f5f5; 794 | } 795 | .bs-docs-sidenav a:hover .icon-chevron-right { 796 | opacity: .5; 797 | } 798 | .bs-docs-sidenav .active .icon-chevron-right, 799 | .bs-docs-sidenav .active a:hover .icon-chevron-right { 800 | background-image: url(../img/glyphicons-halflings-white.png); 801 | opacity: 1; 802 | } 803 | .bs-docs-sidenav.affix { 804 | top: 40px; 805 | } 806 | .bs-docs-sidenav.affix-bottom { 807 | position: absolute; 808 | top: auto; 809 | bottom: 270px; 810 | } 811 | 812 | 813 | 814 | 815 | /* Responsive 816 | -------------------------------------------------- */ 817 | 818 | /* Desktop large 819 | ------------------------- */ 820 | @media (min-width: 1200px) { 821 | .bs-docs-container { 822 | max-width: 970px; 823 | } 824 | .bs-docs-sidenav { 825 | width: 258px; 826 | } 827 | .bs-docs-sidenav > li > a { 828 | width: 230px \9; /* Override the previous IE8-9 hack */ 829 | } 830 | } 831 | 832 | /* Desktop 833 | ------------------------- */ 834 | @media (max-width: 980px) { 835 | /* Unfloat brand */ 836 | body > .navbar-fixed-top .brand { 837 | float: left; 838 | margin-left: 0; 839 | padding-left: 10px; 840 | padding-right: 10px; 841 | } 842 | 843 | /* Inline-block quick links for more spacing */ 844 | .quick-links li { 845 | display: inline-block; 846 | margin: 5px; 847 | } 848 | 849 | /* When affixed, space properly */ 850 | .bs-docs-sidenav { 851 | top: 0; 852 | margin-top: 30px; 853 | margin-right: 0; 854 | } 855 | } 856 | 857 | /* Tablet to desktop 858 | ------------------------- */ 859 | @media (min-width: 768px) and (max-width: 980px) { 860 | /* Remove any padding from the body */ 861 | body { 862 | padding-top: 0; 863 | } 864 | /* Widen masthead and social buttons to fill body padding */ 865 | .jumbotron { 866 | margin-top: -20px; /* Offset bottom margin on .navbar */ 867 | } 868 | /* Adjust sidenav width */ 869 | .bs-docs-sidenav { 870 | width: 166px; 871 | margin-top: 20px; 872 | } 873 | .bs-docs-sidenav.affix { 874 | top: 0; 875 | } 876 | } 877 | 878 | /* Tablet 879 | ------------------------- */ 880 | @media (max-width: 767px) { 881 | /* Remove any padding from the body */ 882 | body { 883 | padding-top: 0; 884 | } 885 | 886 | /* Widen masthead and social buttons to fill body padding */ 887 | .jumbotron { 888 | padding: 40px 20px; 889 | margin-top: -20px; /* Offset bottom margin on .navbar */ 890 | margin-right: -20px; 891 | margin-left: -20px; 892 | } 893 | .masthead h1 { 894 | font-size: 90px; 895 | } 896 | .masthead p, 897 | .masthead .btn { 898 | font-size: 24px; 899 | } 900 | .marketing .span4 { 901 | margin-bottom: 40px; 902 | } 903 | .bs-docs-social { 904 | margin: 0 -20px; 905 | } 906 | 907 | /* Space out the show-grid examples */ 908 | .show-grid [class*="span"] { 909 | margin-bottom: 5px; 910 | } 911 | 912 | /* Sidenav */ 913 | .bs-docs-sidenav { 914 | width: auto; 915 | margin-bottom: 20px; 916 | } 917 | .bs-docs-sidenav.affix { 918 | position: static; 919 | width: auto; 920 | top: 0; 921 | } 922 | 923 | /* Unfloat the back to top link in footer */ 924 | .footer { 925 | margin-left: -20px; 926 | margin-right: -20px; 927 | padding-left: 20px; 928 | padding-right: 20px; 929 | } 930 | .footer p { 931 | margin-bottom: 9px; 932 | } 933 | } 934 | 935 | /* Landscape phones 936 | ------------------------- */ 937 | @media (max-width: 480px) { 938 | /* Remove padding above jumbotron */ 939 | body { 940 | padding-top: 0; 941 | } 942 | 943 | /* Change up some type stuff */ 944 | h2 small { 945 | display: block; 946 | } 947 | 948 | /* Downsize the jumbotrons */ 949 | .jumbotron h1 { 950 | font-size: 45px; 951 | } 952 | .jumbotron p, 953 | .jumbotron .btn { 954 | font-size: 18px; 955 | } 956 | .jumbotron .btn { 957 | display: block; 958 | margin: 0 auto; 959 | } 960 | 961 | /* center align subhead text like the masthead */ 962 | .subhead h1, 963 | .subhead p { 964 | text-align: center; 965 | } 966 | 967 | /* Marketing on home */ 968 | .marketing h1 { 969 | font-size: 30px; 970 | } 971 | .marketing-byline { 972 | font-size: 18px; 973 | } 974 | 975 | /* center example sites */ 976 | .example-sites { 977 | margin-left: 0; 978 | } 979 | .example-sites > li { 980 | float: none; 981 | display: block; 982 | max-width: 280px; 983 | margin: 0 auto 18px; 984 | text-align: center; 985 | } 986 | .example-sites .thumbnail > img { 987 | max-width: 270px; 988 | } 989 | 990 | /* Do our best to make tables work in narrow viewports */ 991 | table code { 992 | white-space: normal; 993 | word-wrap: break-word; 994 | word-break: break-all; 995 | } 996 | 997 | /* Modal example */ 998 | .modal-example .modal { 999 | position: relative; 1000 | top: auto; 1001 | right: auto; 1002 | bottom: auto; 1003 | left: auto; 1004 | } 1005 | 1006 | /* Tighten up footer */ 1007 | .footer { 1008 | padding-top: 20px; 1009 | padding-bottom: 20px; 1010 | } 1011 | /* Unfloat the back to top in footer to prevent odd text wrapping */ 1012 | .footer .pull-right { 1013 | float: none; 1014 | } 1015 | } 1016 | -------------------------------------------------------------------------------- /webapp/stylesheets/bootstrap-responsive.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.2.1 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | 11 | .clearfix { 12 | *zoom: 1; 13 | } 14 | 15 | .clearfix:before, 16 | .clearfix:after { 17 | display: table; 18 | line-height: 0; 19 | content: ""; 20 | } 21 | 22 | .clearfix:after { 23 | clear: both; 24 | } 25 | 26 | .hide-text { 27 | font: 0/0 a; 28 | color: transparent; 29 | text-shadow: none; 30 | background-color: transparent; 31 | border: 0; 32 | } 33 | 34 | .input-block-level { 35 | display: block; 36 | width: 100%; 37 | min-height: 30px; 38 | -webkit-box-sizing: border-box; 39 | -moz-box-sizing: border-box; 40 | box-sizing: border-box; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | visibility: hidden; 46 | } 47 | 48 | .visible-phone { 49 | display: none !important; 50 | } 51 | 52 | .visible-tablet { 53 | display: none !important; 54 | } 55 | 56 | .hidden-desktop { 57 | display: none !important; 58 | } 59 | 60 | .visible-desktop { 61 | display: inherit !important; 62 | } 63 | 64 | @media (min-width: 768px) and (max-width: 979px) { 65 | .hidden-desktop { 66 | display: inherit !important; 67 | } 68 | .visible-desktop { 69 | display: none !important ; 70 | } 71 | .visible-tablet { 72 | display: inherit !important; 73 | } 74 | .hidden-tablet { 75 | display: none !important; 76 | } 77 | } 78 | 79 | @media (max-width: 767px) { 80 | .hidden-desktop { 81 | display: inherit !important; 82 | } 83 | .visible-desktop { 84 | display: none !important; 85 | } 86 | .visible-phone { 87 | display: inherit !important; 88 | } 89 | .hidden-phone { 90 | display: none !important; 91 | } 92 | } 93 | 94 | @media (min-width: 1200px) { 95 | .row { 96 | margin-left: -30px; 97 | *zoom: 1; 98 | } 99 | .row:before, 100 | .row:after { 101 | display: table; 102 | line-height: 0; 103 | content: ""; 104 | } 105 | .row:after { 106 | clear: both; 107 | } 108 | [class*="span"] { 109 | float: left; 110 | min-height: 1px; 111 | margin-left: 30px; 112 | } 113 | .container, 114 | .navbar-static-top .container, 115 | .navbar-fixed-top .container, 116 | .navbar-fixed-bottom .container { 117 | width: 1170px; 118 | } 119 | .span12 { 120 | width: 1170px; 121 | } 122 | .span11 { 123 | width: 1070px; 124 | } 125 | .span10 { 126 | width: 970px; 127 | } 128 | .span9 { 129 | width: 870px; 130 | } 131 | .span8 { 132 | width: 770px; 133 | } 134 | .span7 { 135 | width: 670px; 136 | } 137 | .span6 { 138 | width: 570px; 139 | } 140 | .span5 { 141 | width: 470px; 142 | } 143 | .span4 { 144 | width: 370px; 145 | } 146 | .span3 { 147 | width: 270px; 148 | } 149 | .span2 { 150 | width: 170px; 151 | } 152 | .span1 { 153 | width: 70px; 154 | } 155 | .offset12 { 156 | margin-left: 1230px; 157 | } 158 | .offset11 { 159 | margin-left: 1130px; 160 | } 161 | .offset10 { 162 | margin-left: 1030px; 163 | } 164 | .offset9 { 165 | margin-left: 930px; 166 | } 167 | .offset8 { 168 | margin-left: 830px; 169 | } 170 | .offset7 { 171 | margin-left: 730px; 172 | } 173 | .offset6 { 174 | margin-left: 630px; 175 | } 176 | .offset5 { 177 | margin-left: 530px; 178 | } 179 | .offset4 { 180 | margin-left: 430px; 181 | } 182 | .offset3 { 183 | margin-left: 330px; 184 | } 185 | .offset2 { 186 | margin-left: 230px; 187 | } 188 | .offset1 { 189 | margin-left: 130px; 190 | } 191 | .row-fluid { 192 | width: 100%; 193 | *zoom: 1; 194 | } 195 | .row-fluid:before, 196 | .row-fluid:after { 197 | display: table; 198 | line-height: 0; 199 | content: ""; 200 | } 201 | .row-fluid:after { 202 | clear: both; 203 | } 204 | .row-fluid [class*="span"] { 205 | display: block; 206 | float: left; 207 | width: 100%; 208 | min-height: 30px; 209 | margin-left: 2.564102564102564%; 210 | *margin-left: 2.5109110747408616%; 211 | -webkit-box-sizing: border-box; 212 | -moz-box-sizing: border-box; 213 | box-sizing: border-box; 214 | } 215 | .row-fluid [class*="span"]:first-child { 216 | margin-left: 0; 217 | } 218 | .row-fluid .controls-row [class*="span"] + [class*="span"] { 219 | margin-left: 2.564102564102564%; 220 | } 221 | .row-fluid .span12 { 222 | width: 100%; 223 | *width: 99.94680851063829%; 224 | } 225 | .row-fluid .span11 { 226 | width: 91.45299145299145%; 227 | *width: 91.39979996362975%; 228 | } 229 | .row-fluid .span10 { 230 | width: 82.90598290598291%; 231 | *width: 82.8527914166212%; 232 | } 233 | .row-fluid .span9 { 234 | width: 74.35897435897436%; 235 | *width: 74.30578286961266%; 236 | } 237 | .row-fluid .span8 { 238 | width: 65.81196581196582%; 239 | *width: 65.75877432260411%; 240 | } 241 | .row-fluid .span7 { 242 | width: 57.26495726495726%; 243 | *width: 57.21176577559556%; 244 | } 245 | .row-fluid .span6 { 246 | width: 48.717948717948715%; 247 | *width: 48.664757228587014%; 248 | } 249 | .row-fluid .span5 { 250 | width: 40.17094017094017%; 251 | *width: 40.11774868157847%; 252 | } 253 | .row-fluid .span4 { 254 | width: 31.623931623931625%; 255 | *width: 31.570740134569924%; 256 | } 257 | .row-fluid .span3 { 258 | width: 23.076923076923077%; 259 | *width: 23.023731587561375%; 260 | } 261 | .row-fluid .span2 { 262 | width: 14.52991452991453%; 263 | *width: 14.476723040552828%; 264 | } 265 | .row-fluid .span1 { 266 | width: 5.982905982905983%; 267 | *width: 5.929714493544281%; 268 | } 269 | .row-fluid .offset12 { 270 | margin-left: 105.12820512820512%; 271 | *margin-left: 105.02182214948171%; 272 | } 273 | .row-fluid .offset12:first-child { 274 | margin-left: 102.56410256410257%; 275 | *margin-left: 102.45771958537915%; 276 | } 277 | .row-fluid .offset11 { 278 | margin-left: 96.58119658119658%; 279 | *margin-left: 96.47481360247316%; 280 | } 281 | .row-fluid .offset11:first-child { 282 | margin-left: 94.01709401709402%; 283 | *margin-left: 93.91071103837061%; 284 | } 285 | .row-fluid .offset10 { 286 | margin-left: 88.03418803418803%; 287 | *margin-left: 87.92780505546462%; 288 | } 289 | .row-fluid .offset10:first-child { 290 | margin-left: 85.47008547008548%; 291 | *margin-left: 85.36370249136206%; 292 | } 293 | .row-fluid .offset9 { 294 | margin-left: 79.48717948717949%; 295 | *margin-left: 79.38079650845607%; 296 | } 297 | .row-fluid .offset9:first-child { 298 | margin-left: 76.92307692307693%; 299 | *margin-left: 76.81669394435352%; 300 | } 301 | .row-fluid .offset8 { 302 | margin-left: 70.94017094017094%; 303 | *margin-left: 70.83378796144753%; 304 | } 305 | .row-fluid .offset8:first-child { 306 | margin-left: 68.37606837606839%; 307 | *margin-left: 68.26968539734497%; 308 | } 309 | .row-fluid .offset7 { 310 | margin-left: 62.393162393162385%; 311 | *margin-left: 62.28677941443899%; 312 | } 313 | .row-fluid .offset7:first-child { 314 | margin-left: 59.82905982905982%; 315 | *margin-left: 59.72267685033642%; 316 | } 317 | .row-fluid .offset6 { 318 | margin-left: 53.84615384615384%; 319 | *margin-left: 53.739770867430444%; 320 | } 321 | .row-fluid .offset6:first-child { 322 | margin-left: 51.28205128205128%; 323 | *margin-left: 51.175668303327875%; 324 | } 325 | .row-fluid .offset5 { 326 | margin-left: 45.299145299145295%; 327 | *margin-left: 45.1927623204219%; 328 | } 329 | .row-fluid .offset5:first-child { 330 | margin-left: 42.73504273504273%; 331 | *margin-left: 42.62865975631933%; 332 | } 333 | .row-fluid .offset4 { 334 | margin-left: 36.75213675213675%; 335 | *margin-left: 36.645753773413354%; 336 | } 337 | .row-fluid .offset4:first-child { 338 | margin-left: 34.18803418803419%; 339 | *margin-left: 34.081651209310785%; 340 | } 341 | .row-fluid .offset3 { 342 | margin-left: 28.205128205128204%; 343 | *margin-left: 28.0987452264048%; 344 | } 345 | .row-fluid .offset3:first-child { 346 | margin-left: 25.641025641025642%; 347 | *margin-left: 25.53464266230224%; 348 | } 349 | .row-fluid .offset2 { 350 | margin-left: 19.65811965811966%; 351 | *margin-left: 19.551736679396257%; 352 | } 353 | .row-fluid .offset2:first-child { 354 | margin-left: 17.094017094017094%; 355 | *margin-left: 16.98763411529369%; 356 | } 357 | .row-fluid .offset1 { 358 | margin-left: 11.11111111111111%; 359 | *margin-left: 11.004728132387708%; 360 | } 361 | .row-fluid .offset1:first-child { 362 | margin-left: 8.547008547008547%; 363 | *margin-left: 8.440625568285142%; 364 | } 365 | input, 366 | textarea, 367 | .uneditable-input { 368 | margin-left: 0; 369 | } 370 | .controls-row [class*="span"] + [class*="span"] { 371 | margin-left: 30px; 372 | } 373 | input.span12, 374 | textarea.span12, 375 | .uneditable-input.span12 { 376 | width: 1156px; 377 | } 378 | input.span11, 379 | textarea.span11, 380 | .uneditable-input.span11 { 381 | width: 1056px; 382 | } 383 | input.span10, 384 | textarea.span10, 385 | .uneditable-input.span10 { 386 | width: 956px; 387 | } 388 | input.span9, 389 | textarea.span9, 390 | .uneditable-input.span9 { 391 | width: 856px; 392 | } 393 | input.span8, 394 | textarea.span8, 395 | .uneditable-input.span8 { 396 | width: 756px; 397 | } 398 | input.span7, 399 | textarea.span7, 400 | .uneditable-input.span7 { 401 | width: 656px; 402 | } 403 | input.span6, 404 | textarea.span6, 405 | .uneditable-input.span6 { 406 | width: 556px; 407 | } 408 | input.span5, 409 | textarea.span5, 410 | .uneditable-input.span5 { 411 | width: 456px; 412 | } 413 | input.span4, 414 | textarea.span4, 415 | .uneditable-input.span4 { 416 | width: 356px; 417 | } 418 | input.span3, 419 | textarea.span3, 420 | .uneditable-input.span3 { 421 | width: 256px; 422 | } 423 | input.span2, 424 | textarea.span2, 425 | .uneditable-input.span2 { 426 | width: 156px; 427 | } 428 | input.span1, 429 | textarea.span1, 430 | .uneditable-input.span1 { 431 | width: 56px; 432 | } 433 | .thumbnails { 434 | margin-left: -30px; 435 | } 436 | .thumbnails > li { 437 | margin-left: 30px; 438 | } 439 | .row-fluid .thumbnails { 440 | margin-left: 0; 441 | } 442 | } 443 | 444 | @media (min-width: 768px) and (max-width: 979px) { 445 | .row { 446 | margin-left: -20px; 447 | *zoom: 1; 448 | } 449 | .row:before, 450 | .row:after { 451 | display: table; 452 | line-height: 0; 453 | content: ""; 454 | } 455 | .row:after { 456 | clear: both; 457 | } 458 | [class*="span"] { 459 | float: left; 460 | min-height: 1px; 461 | margin-left: 20px; 462 | } 463 | .container, 464 | .navbar-static-top .container, 465 | .navbar-fixed-top .container, 466 | .navbar-fixed-bottom .container { 467 | width: 724px; 468 | } 469 | .span12 { 470 | width: 724px; 471 | } 472 | .span11 { 473 | width: 662px; 474 | } 475 | .span10 { 476 | width: 600px; 477 | } 478 | .span9 { 479 | width: 538px; 480 | } 481 | .span8 { 482 | width: 476px; 483 | } 484 | .span7 { 485 | width: 414px; 486 | } 487 | .span6 { 488 | width: 352px; 489 | } 490 | .span5 { 491 | width: 290px; 492 | } 493 | .span4 { 494 | width: 228px; 495 | } 496 | .span3 { 497 | width: 166px; 498 | } 499 | .span2 { 500 | width: 104px; 501 | } 502 | .span1 { 503 | width: 42px; 504 | } 505 | .offset12 { 506 | margin-left: 764px; 507 | } 508 | .offset11 { 509 | margin-left: 702px; 510 | } 511 | .offset10 { 512 | margin-left: 640px; 513 | } 514 | .offset9 { 515 | margin-left: 578px; 516 | } 517 | .offset8 { 518 | margin-left: 516px; 519 | } 520 | .offset7 { 521 | margin-left: 454px; 522 | } 523 | .offset6 { 524 | margin-left: 392px; 525 | } 526 | .offset5 { 527 | margin-left: 330px; 528 | } 529 | .offset4 { 530 | margin-left: 268px; 531 | } 532 | .offset3 { 533 | margin-left: 206px; 534 | } 535 | .offset2 { 536 | margin-left: 144px; 537 | } 538 | .offset1 { 539 | margin-left: 82px; 540 | } 541 | .row-fluid { 542 | width: 100%; 543 | *zoom: 1; 544 | } 545 | .row-fluid:before, 546 | .row-fluid:after { 547 | display: table; 548 | line-height: 0; 549 | content: ""; 550 | } 551 | .row-fluid:after { 552 | clear: both; 553 | } 554 | .row-fluid [class*="span"] { 555 | display: block; 556 | float: left; 557 | width: 100%; 558 | min-height: 30px; 559 | margin-left: 2.7624309392265194%; 560 | *margin-left: 2.709239449864817%; 561 | -webkit-box-sizing: border-box; 562 | -moz-box-sizing: border-box; 563 | box-sizing: border-box; 564 | } 565 | .row-fluid [class*="span"]:first-child { 566 | margin-left: 0; 567 | } 568 | .row-fluid .controls-row [class*="span"] + [class*="span"] { 569 | margin-left: 2.7624309392265194%; 570 | } 571 | .row-fluid .span12 { 572 | width: 100%; 573 | *width: 99.94680851063829%; 574 | } 575 | .row-fluid .span11 { 576 | width: 91.43646408839778%; 577 | *width: 91.38327259903608%; 578 | } 579 | .row-fluid .span10 { 580 | width: 82.87292817679558%; 581 | *width: 82.81973668743387%; 582 | } 583 | .row-fluid .span9 { 584 | width: 74.30939226519337%; 585 | *width: 74.25620077583166%; 586 | } 587 | .row-fluid .span8 { 588 | width: 65.74585635359117%; 589 | *width: 65.69266486422946%; 590 | } 591 | .row-fluid .span7 { 592 | width: 57.18232044198895%; 593 | *width: 57.12912895262725%; 594 | } 595 | .row-fluid .span6 { 596 | width: 48.61878453038674%; 597 | *width: 48.56559304102504%; 598 | } 599 | .row-fluid .span5 { 600 | width: 40.05524861878453%; 601 | *width: 40.00205712942283%; 602 | } 603 | .row-fluid .span4 { 604 | width: 31.491712707182323%; 605 | *width: 31.43852121782062%; 606 | } 607 | .row-fluid .span3 { 608 | width: 22.92817679558011%; 609 | *width: 22.87498530621841%; 610 | } 611 | .row-fluid .span2 { 612 | width: 14.3646408839779%; 613 | *width: 14.311449394616199%; 614 | } 615 | .row-fluid .span1 { 616 | width: 5.801104972375691%; 617 | *width: 5.747913483013988%; 618 | } 619 | .row-fluid .offset12 { 620 | margin-left: 105.52486187845304%; 621 | *margin-left: 105.41847889972962%; 622 | } 623 | .row-fluid .offset12:first-child { 624 | margin-left: 102.76243093922652%; 625 | *margin-left: 102.6560479605031%; 626 | } 627 | .row-fluid .offset11 { 628 | margin-left: 96.96132596685082%; 629 | *margin-left: 96.8549429881274%; 630 | } 631 | .row-fluid .offset11:first-child { 632 | margin-left: 94.1988950276243%; 633 | *margin-left: 94.09251204890089%; 634 | } 635 | .row-fluid .offset10 { 636 | margin-left: 88.39779005524862%; 637 | *margin-left: 88.2914070765252%; 638 | } 639 | .row-fluid .offset10:first-child { 640 | margin-left: 85.6353591160221%; 641 | *margin-left: 85.52897613729868%; 642 | } 643 | .row-fluid .offset9 { 644 | margin-left: 79.8342541436464%; 645 | *margin-left: 79.72787116492299%; 646 | } 647 | .row-fluid .offset9:first-child { 648 | margin-left: 77.07182320441989%; 649 | *margin-left: 76.96544022569647%; 650 | } 651 | .row-fluid .offset8 { 652 | margin-left: 71.2707182320442%; 653 | *margin-left: 71.16433525332079%; 654 | } 655 | .row-fluid .offset8:first-child { 656 | margin-left: 68.50828729281768%; 657 | *margin-left: 68.40190431409427%; 658 | } 659 | .row-fluid .offset7 { 660 | margin-left: 62.70718232044199%; 661 | *margin-left: 62.600799341718584%; 662 | } 663 | .row-fluid .offset7:first-child { 664 | margin-left: 59.94475138121547%; 665 | *margin-left: 59.838368402492065%; 666 | } 667 | .row-fluid .offset6 { 668 | margin-left: 54.14364640883978%; 669 | *margin-left: 54.037263430116376%; 670 | } 671 | .row-fluid .offset6:first-child { 672 | margin-left: 51.38121546961326%; 673 | *margin-left: 51.27483249088986%; 674 | } 675 | .row-fluid .offset5 { 676 | margin-left: 45.58011049723757%; 677 | *margin-left: 45.47372751851417%; 678 | } 679 | .row-fluid .offset5:first-child { 680 | margin-left: 42.81767955801105%; 681 | *margin-left: 42.71129657928765%; 682 | } 683 | .row-fluid .offset4 { 684 | margin-left: 37.01657458563536%; 685 | *margin-left: 36.91019160691196%; 686 | } 687 | .row-fluid .offset4:first-child { 688 | margin-left: 34.25414364640884%; 689 | *margin-left: 34.14776066768544%; 690 | } 691 | .row-fluid .offset3 { 692 | margin-left: 28.45303867403315%; 693 | *margin-left: 28.346655695309746%; 694 | } 695 | .row-fluid .offset3:first-child { 696 | margin-left: 25.69060773480663%; 697 | *margin-left: 25.584224756083227%; 698 | } 699 | .row-fluid .offset2 { 700 | margin-left: 19.88950276243094%; 701 | *margin-left: 19.783119783707537%; 702 | } 703 | .row-fluid .offset2:first-child { 704 | margin-left: 17.12707182320442%; 705 | *margin-left: 17.02068884448102%; 706 | } 707 | .row-fluid .offset1 { 708 | margin-left: 11.32596685082873%; 709 | *margin-left: 11.219583872105325%; 710 | } 711 | .row-fluid .offset1:first-child { 712 | margin-left: 8.56353591160221%; 713 | *margin-left: 8.457152932878806%; 714 | } 715 | input, 716 | textarea, 717 | .uneditable-input { 718 | margin-left: 0; 719 | } 720 | .controls-row [class*="span"] + [class*="span"] { 721 | margin-left: 20px; 722 | } 723 | input.span12, 724 | textarea.span12, 725 | .uneditable-input.span12 { 726 | width: 710px; 727 | } 728 | input.span11, 729 | textarea.span11, 730 | .uneditable-input.span11 { 731 | width: 648px; 732 | } 733 | input.span10, 734 | textarea.span10, 735 | .uneditable-input.span10 { 736 | width: 586px; 737 | } 738 | input.span9, 739 | textarea.span9, 740 | .uneditable-input.span9 { 741 | width: 524px; 742 | } 743 | input.span8, 744 | textarea.span8, 745 | .uneditable-input.span8 { 746 | width: 462px; 747 | } 748 | input.span7, 749 | textarea.span7, 750 | .uneditable-input.span7 { 751 | width: 400px; 752 | } 753 | input.span6, 754 | textarea.span6, 755 | .uneditable-input.span6 { 756 | width: 338px; 757 | } 758 | input.span5, 759 | textarea.span5, 760 | .uneditable-input.span5 { 761 | width: 276px; 762 | } 763 | input.span4, 764 | textarea.span4, 765 | .uneditable-input.span4 { 766 | width: 214px; 767 | } 768 | input.span3, 769 | textarea.span3, 770 | .uneditable-input.span3 { 771 | width: 152px; 772 | } 773 | input.span2, 774 | textarea.span2, 775 | .uneditable-input.span2 { 776 | width: 90px; 777 | } 778 | input.span1, 779 | textarea.span1, 780 | .uneditable-input.span1 { 781 | width: 28px; 782 | } 783 | } 784 | 785 | @media (max-width: 767px) { 786 | body { 787 | padding-right: 20px; 788 | padding-left: 20px; 789 | } 790 | .navbar-fixed-top, 791 | .navbar-fixed-bottom, 792 | .navbar-static-top { 793 | margin-right: -20px; 794 | margin-left: -20px; 795 | } 796 | .container-fluid { 797 | padding: 0; 798 | } 799 | .dl-horizontal dt { 800 | float: none; 801 | width: auto; 802 | clear: none; 803 | text-align: left; 804 | } 805 | .dl-horizontal dd { 806 | margin-left: 0; 807 | } 808 | .container { 809 | width: auto; 810 | } 811 | .row-fluid { 812 | width: 100%; 813 | } 814 | .row, 815 | .thumbnails { 816 | margin-left: 0; 817 | } 818 | .thumbnails > li { 819 | float: none; 820 | margin-left: 0; 821 | } 822 | [class*="span"], 823 | .uneditable-input[class*="span"], 824 | .row-fluid [class*="span"] { 825 | display: block; 826 | float: none; 827 | width: 100%; 828 | margin-left: 0; 829 | -webkit-box-sizing: border-box; 830 | -moz-box-sizing: border-box; 831 | box-sizing: border-box; 832 | } 833 | .span12, 834 | .row-fluid .span12 { 835 | width: 100%; 836 | -webkit-box-sizing: border-box; 837 | -moz-box-sizing: border-box; 838 | box-sizing: border-box; 839 | } 840 | .row-fluid [class*="offset"]:first-child { 841 | margin-left: 0; 842 | } 843 | .input-large, 844 | .input-xlarge, 845 | .input-xxlarge, 846 | input[class*="span"], 847 | select[class*="span"], 848 | textarea[class*="span"], 849 | .uneditable-input { 850 | display: block; 851 | width: 100%; 852 | min-height: 30px; 853 | -webkit-box-sizing: border-box; 854 | -moz-box-sizing: border-box; 855 | box-sizing: border-box; 856 | } 857 | .input-prepend input, 858 | .input-append input, 859 | .input-prepend input[class*="span"], 860 | .input-append input[class*="span"] { 861 | display: inline-block; 862 | width: auto; 863 | } 864 | .controls-row [class*="span"] + [class*="span"] { 865 | margin-left: 0; 866 | } 867 | .modal { 868 | position: fixed; 869 | top: 20px; 870 | right: 20px; 871 | left: 20px; 872 | width: auto; 873 | margin: 0; 874 | } 875 | .modal.fade { 876 | top: -100px; 877 | } 878 | .modal.fade.in { 879 | top: 20px; 880 | } 881 | } 882 | 883 | @media (max-width: 480px) { 884 | .nav-collapse { 885 | -webkit-transform: translate3d(0, 0, 0); 886 | } 887 | .page-header h1 small { 888 | display: block; 889 | line-height: 20px; 890 | } 891 | input[type="checkbox"], 892 | input[type="radio"] { 893 | border: 1px solid #ccc; 894 | } 895 | .form-horizontal .control-label { 896 | float: none; 897 | width: auto; 898 | padding-top: 0; 899 | text-align: left; 900 | } 901 | .form-horizontal .controls { 902 | margin-left: 0; 903 | } 904 | .form-horizontal .control-list { 905 | padding-top: 0; 906 | } 907 | .form-horizontal .form-actions { 908 | padding-right: 10px; 909 | padding-left: 10px; 910 | } 911 | .media .pull-left, 912 | .media .pull-right { 913 | display: block; 914 | float: none; 915 | margin-bottom: 10px; 916 | } 917 | .media-object { 918 | margin-right: 0; 919 | margin-left: 0; 920 | } 921 | .modal { 922 | top: 10px; 923 | right: 10px; 924 | left: 10px; 925 | } 926 | .modal-header .close { 927 | padding: 10px; 928 | margin: -10px; 929 | } 930 | .carousel-caption { 931 | position: static; 932 | } 933 | } 934 | 935 | @media (max-width: 979px) { 936 | body { 937 | padding-top: 0; 938 | } 939 | .navbar-fixed-top, 940 | .navbar-fixed-bottom { 941 | position: static; 942 | } 943 | .navbar-fixed-top { 944 | margin-bottom: 20px; 945 | } 946 | .navbar-fixed-bottom { 947 | margin-top: 20px; 948 | } 949 | .navbar-fixed-top .navbar-inner, 950 | .navbar-fixed-bottom .navbar-inner { 951 | padding: 5px; 952 | } 953 | .navbar .container { 954 | width: auto; 955 | padding: 0; 956 | } 957 | .navbar .brand { 958 | padding-right: 10px; 959 | padding-left: 10px; 960 | margin: 0 0 0 -5px; 961 | } 962 | .nav-collapse { 963 | clear: both; 964 | } 965 | .nav-collapse .nav { 966 | float: none; 967 | margin: 0 0 10px; 968 | } 969 | .nav-collapse .nav > li { 970 | float: none; 971 | } 972 | .nav-collapse .nav > li > a { 973 | margin-bottom: 2px; 974 | } 975 | .nav-collapse .nav > .divider-vertical { 976 | display: none; 977 | } 978 | .nav-collapse .nav .nav-header { 979 | color: #777777; 980 | text-shadow: none; 981 | } 982 | .nav-collapse .nav > li > a, 983 | .nav-collapse .dropdown-menu a { 984 | padding: 9px 15px; 985 | font-weight: bold; 986 | color: #777777; 987 | -webkit-border-radius: 3px; 988 | -moz-border-radius: 3px; 989 | border-radius: 3px; 990 | } 991 | .nav-collapse .btn { 992 | padding: 4px 10px 4px; 993 | font-weight: normal; 994 | -webkit-border-radius: 4px; 995 | -moz-border-radius: 4px; 996 | border-radius: 4px; 997 | } 998 | .nav-collapse .dropdown-menu li + li a { 999 | margin-bottom: 2px; 1000 | } 1001 | .nav-collapse .nav > li > a:hover, 1002 | .nav-collapse .dropdown-menu a:hover { 1003 | background-color: #f2f2f2; 1004 | } 1005 | .navbar-inverse .nav-collapse .nav > li > a, 1006 | .navbar-inverse .nav-collapse .dropdown-menu a { 1007 | color: #999999; 1008 | } 1009 | .navbar-inverse .nav-collapse .nav > li > a:hover, 1010 | .navbar-inverse .nav-collapse .dropdown-menu a:hover { 1011 | background-color: #111111; 1012 | } 1013 | .nav-collapse.in .btn-group { 1014 | padding: 0; 1015 | margin-top: 5px; 1016 | } 1017 | .nav-collapse .dropdown-menu { 1018 | position: static; 1019 | top: auto; 1020 | left: auto; 1021 | display: none; 1022 | float: none; 1023 | max-width: none; 1024 | padding: 0; 1025 | margin: 0 15px; 1026 | background-color: transparent; 1027 | border: none; 1028 | -webkit-border-radius: 0; 1029 | -moz-border-radius: 0; 1030 | border-radius: 0; 1031 | -webkit-box-shadow: none; 1032 | -moz-box-shadow: none; 1033 | box-shadow: none; 1034 | } 1035 | .nav-collapse .open > .dropdown-menu { 1036 | display: block; 1037 | } 1038 | .nav-collapse .dropdown-menu:before, 1039 | .nav-collapse .dropdown-menu:after { 1040 | display: none; 1041 | } 1042 | .nav-collapse .dropdown-menu .divider { 1043 | display: none; 1044 | } 1045 | .nav-collapse .nav > li > .dropdown-menu:before, 1046 | .nav-collapse .nav > li > .dropdown-menu:after { 1047 | display: none; 1048 | } 1049 | .nav-collapse .navbar-form, 1050 | .nav-collapse .navbar-search { 1051 | float: none; 1052 | padding: 10px 15px; 1053 | margin: 10px 0; 1054 | border-top: 1px solid #f2f2f2; 1055 | border-bottom: 1px solid #f2f2f2; 1056 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1057 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1058 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1059 | } 1060 | .navbar-inverse .nav-collapse .navbar-form, 1061 | .navbar-inverse .nav-collapse .navbar-search { 1062 | border-top-color: #111111; 1063 | border-bottom-color: #111111; 1064 | } 1065 | .navbar .nav-collapse .nav.pull-right { 1066 | float: none; 1067 | margin-left: 0; 1068 | } 1069 | .nav-collapse, 1070 | .nav-collapse.collapse { 1071 | height: 0; 1072 | overflow: hidden; 1073 | } 1074 | .navbar .btn-navbar { 1075 | display: block; 1076 | } 1077 | .navbar-static .navbar-inner { 1078 | padding-right: 10px; 1079 | padding-left: 10px; 1080 | } 1081 | } 1082 | 1083 | @media (min-width: 980px) { 1084 | .nav-collapse.collapse { 1085 | height: auto !important; 1086 | overflow: visible !important; 1087 | } 1088 | } 1089 | --------------------------------------------------------------------------------