├── .gitignore ├── Procfile ├── README.md ├── pom.xml ├── src ├── main │ ├── java │ │ ├── db │ │ │ └── DataBase.java │ │ ├── model │ │ │ └── User.java │ │ ├── util │ │ │ ├── HttpRequestUtils.java │ │ │ └── IOUtils.java │ │ └── webserver │ │ │ ├── RequestHandler.java │ │ │ └── WebServer.java │ └── resources │ │ └── logback.xml └── test │ └── java │ └── util │ ├── HttpRequestUtilsTest.java │ └── IOUtilsTest.java └── webapp ├── css ├── bootstrap.min.css └── styles.css ├── favicon.ico ├── fonts ├── glyphicons-halflings-regular.eot ├── glyphicons-halflings-regular.svg ├── glyphicons-halflings-regular.ttf ├── glyphicons-halflings-regular.woff └── glyphicons-halflings-regular.woff2 ├── images └── 80-text.png ├── index.html ├── js ├── bootstrap.min.js ├── jquery-2.2.0.min.js └── scripts.js ├── qna ├── form.html └── show.html └── user ├── form.html ├── list.html ├── login.html ├── login_failed.html └── profile.html /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /.settings/ 3 | /target/ 4 | /.classpath 5 | /.project 6 | /.idea 7 | *.iml 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: java -cp target/classes:target/dependency/* webserver.WebServer $PORT -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 실습을 위한 개발 환경 세팅 2 | * https://github.com/slipp/web-application-server 프로젝트를 자신의 계정으로 Fork한다. Github 우측 상단의 Fork 버튼을 클릭하면 자신의 계정으로 Fork된다. 3 | * Fork한 프로젝트를 eclipse 또는 터미널에서 clone 한다. 4 | * Fork한 프로젝트를 eclipse로 import한 후에 Maven 빌드 도구를 활용해 eclipse 프로젝트로 변환한다.(mvn eclipse:clean eclipse:eclipse) 5 | * 빌드가 성공하면 반드시 refresh(fn + f5)를 실행해야 한다. 6 | 7 | # 웹 서버 시작 및 테스트 8 | * webserver.WebServer 는 사용자의 요청을 받아 RequestHandler에 작업을 위임하는 클래스이다. 9 | * 사용자 요청에 대한 모든 처리는 RequestHandler 클래스의 run() 메서드가 담당한다. 10 | * WebServer를 실행한 후 브라우저에서 http://localhost:8080으로 접속해 "Hello World" 메시지가 출력되는지 확인한다. 11 | 12 | # 각 요구사항별 학습 내용 정리 13 | * 구현 단계에서는 각 요구사항을 구현하는데 집중한다. 14 | * 구현을 완료한 후 구현 과정에서 새롭게 알게된 내용, 궁금한 내용을 기록한다. 15 | * 각 요구사항을 구현하는 것이 중요한 것이 아니라 구현 과정을 통해 학습한 내용을 인식하는 것이 배움에 중요하다. 16 | 17 | ### 요구사항 1 - http://localhost:8080/index.html로 접속시 응답 18 | * 19 | 20 | ### 요구사항 2 - get 방식으로 회원가입 21 | * 22 | 23 | ### 요구사항 3 - post 방식으로 회원가입 24 | * 25 | 26 | ### 요구사항 4 - redirect 방식으로 이동 27 | * 28 | 29 | ### 요구사항 5 - cookie 30 | * 31 | 32 | ### 요구사항 6 - stylesheet 적용 33 | * 34 | 35 | ### heroku 서버에 배포 후 36 | * -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | org.nhnnext 5 | web-application-server 6 | 1.0 7 | jar 8 | 9 | 10 | UTF-8 11 | UTF-8 12 | 13 | 14 | 15 | 16 | 17 | junit 18 | junit 19 | 4.11 20 | test 21 | 22 | 23 | 24 | com.google.guava 25 | guava 26 | 18.0 27 | 28 | 29 | 30 | 31 | ch.qos.logback 32 | logback-classic 33 | 1.1.2 34 | 35 | 36 | 37 | 38 | web-application-server 39 | src/main/java 40 | src/test/java 41 | target/test-classes 42 | 43 | 44 | 45 | src/main/resources 46 | 47 | 48 | 49 | 50 | 51 | maven-eclipse-plugin 52 | 2.9 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-compiler-plugin 57 | 3.1 58 | 59 | 1.8 60 | 1.8 61 | utf-8 62 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-dependency-plugin 67 | 2.4 68 | 69 | 70 | copy-dependencies 71 | package 72 | 73 | copy-dependencies 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/main/java/db/DataBase.java: -------------------------------------------------------------------------------- 1 | package db; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | 6 | import com.google.common.collect.Maps; 7 | 8 | import model.User; 9 | 10 | public class DataBase { 11 | private static Map users = Maps.newHashMap(); 12 | 13 | public static void addUser(User user) { 14 | users.put(user.getUserId(), user); 15 | } 16 | 17 | public static User findUserById(String userId) { 18 | return users.get(userId); 19 | } 20 | 21 | public static Collection findAll() { 22 | return users.values(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/model/User.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | public class User { 4 | private String userId; 5 | private String password; 6 | private String name; 7 | private String email; 8 | 9 | public User(String userId, String password, String name, String email) { 10 | this.userId = userId; 11 | this.password = password; 12 | this.name = name; 13 | this.email = email; 14 | } 15 | 16 | public String getUserId() { 17 | return userId; 18 | } 19 | 20 | public String getPassword() { 21 | return password; 22 | } 23 | 24 | public String getName() { 25 | return name; 26 | } 27 | 28 | public String getEmail() { 29 | return email; 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return "User [userId=" + userId + ", password=" + password + ", name=" + name + ", email=" + email + "]"; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/util/HttpRequestUtils.java: -------------------------------------------------------------------------------- 1 | package util; 2 | 3 | import java.util.Arrays; 4 | import java.util.Map; 5 | import java.util.stream.Collectors; 6 | 7 | import com.google.common.base.Strings; 8 | import com.google.common.collect.Maps; 9 | 10 | public class HttpRequestUtils { 11 | /** 12 | * @param queryString은 13 | * URL에서 ? 이후에 전달되는 field1=value1&field2=value2 형식임 14 | * @return 15 | */ 16 | public static Map parseQueryString(String queryString) { 17 | return parseValues(queryString, "&"); 18 | } 19 | 20 | /** 21 | * @param 쿠키 22 | * 값은 name1=value1; name2=value2 형식임 23 | * @return 24 | */ 25 | public static Map parseCookies(String cookies) { 26 | return parseValues(cookies, ";"); 27 | } 28 | 29 | private static Map parseValues(String values, String separator) { 30 | if (Strings.isNullOrEmpty(values)) { 31 | return Maps.newHashMap(); 32 | } 33 | 34 | String[] tokens = values.split(separator); 35 | return Arrays.stream(tokens).map(t -> getKeyValue(t, "=")).filter(p -> p != null) 36 | .collect(Collectors.toMap(p -> p.getKey(), p -> p.getValue())); 37 | } 38 | 39 | static Pair getKeyValue(String keyValue, String regex) { 40 | if (Strings.isNullOrEmpty(keyValue)) { 41 | return null; 42 | } 43 | 44 | String[] tokens = keyValue.split(regex); 45 | if (tokens.length != 2) { 46 | return null; 47 | } 48 | 49 | return new Pair(tokens[0], tokens[1]); 50 | } 51 | 52 | public static Pair parseHeader(String header) { 53 | return getKeyValue(header, ": "); 54 | } 55 | 56 | public static class Pair { 57 | String key; 58 | String value; 59 | 60 | Pair(String key, String value) { 61 | this.key = key.trim(); 62 | this.value = value.trim(); 63 | } 64 | 65 | public String getKey() { 66 | return key; 67 | } 68 | 69 | public String getValue() { 70 | return value; 71 | } 72 | 73 | @Override 74 | public int hashCode() { 75 | final int prime = 31; 76 | int result = 1; 77 | result = prime * result + ((key == null) ? 0 : key.hashCode()); 78 | result = prime * result + ((value == null) ? 0 : value.hashCode()); 79 | return result; 80 | } 81 | 82 | @Override 83 | public boolean equals(Object obj) { 84 | if (this == obj) 85 | return true; 86 | if (obj == null) 87 | return false; 88 | if (getClass() != obj.getClass()) 89 | return false; 90 | Pair other = (Pair) obj; 91 | if (key == null) { 92 | if (other.key != null) 93 | return false; 94 | } else if (!key.equals(other.key)) 95 | return false; 96 | if (value == null) { 97 | if (other.value != null) 98 | return false; 99 | } else if (!value.equals(other.value)) 100 | return false; 101 | return true; 102 | } 103 | 104 | @Override 105 | public String toString() { 106 | return "Pair [key=" + key + ", value=" + value + "]"; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/util/IOUtils.java: -------------------------------------------------------------------------------- 1 | package util; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | 6 | public class IOUtils { 7 | /** 8 | * @param BufferedReader는 9 | * Request Body를 시작하는 시점이어야 10 | * @param contentLength는 11 | * Request Header의 Content-Length 값이다. 12 | * @return 13 | * @throws IOException 14 | */ 15 | public static String readData(BufferedReader br, int contentLength) throws IOException { 16 | char[] body = new char[contentLength]; 17 | br.read(body, 0, contentLength); 18 | return String.copyValueOf(body); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/webserver/RequestHandler.java: -------------------------------------------------------------------------------- 1 | package webserver; 2 | 3 | import java.io.DataOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.net.Socket; 8 | 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | public class RequestHandler extends Thread { 13 | private static final Logger log = LoggerFactory.getLogger(RequestHandler.class); 14 | 15 | private Socket connection; 16 | 17 | public RequestHandler(Socket connectionSocket) { 18 | this.connection = connectionSocket; 19 | } 20 | 21 | public void run() { 22 | log.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(), 23 | connection.getPort()); 24 | 25 | try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) { 26 | // TODO 사용자 요청에 대한 처리는 이 곳에 구현하면 된다. 27 | DataOutputStream dos = new DataOutputStream(out); 28 | byte[] body = "Hello World".getBytes(); 29 | response200Header(dos, body.length); 30 | responseBody(dos, body); 31 | } catch (IOException e) { 32 | log.error(e.getMessage()); 33 | } 34 | } 35 | 36 | private void response200Header(DataOutputStream dos, int lengthOfBodyContent) { 37 | try { 38 | dos.writeBytes("HTTP/1.1 200 OK \r\n"); 39 | dos.writeBytes("Content-Type: text/html;charset=utf-8\r\n"); 40 | dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n"); 41 | dos.writeBytes("\r\n"); 42 | } catch (IOException e) { 43 | log.error(e.getMessage()); 44 | } 45 | } 46 | 47 | private void responseBody(DataOutputStream dos, byte[] body) { 48 | try { 49 | dos.write(body, 0, body.length); 50 | dos.flush(); 51 | } catch (IOException e) { 52 | log.error(e.getMessage()); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/webserver/WebServer.java: -------------------------------------------------------------------------------- 1 | package webserver; 2 | 3 | import java.net.ServerSocket; 4 | import java.net.Socket; 5 | 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | public class WebServer { 10 | private static final Logger log = LoggerFactory.getLogger(WebServer.class); 11 | private static final int DEFAULT_PORT = 8080; 12 | 13 | public static void main(String args[]) throws Exception { 14 | int port = 0; 15 | if (args == null || args.length == 0) { 16 | port = DEFAULT_PORT; 17 | } else { 18 | port = Integer.parseInt(args[0]); 19 | } 20 | 21 | // 서버소켓을 생성한다. 웹서버는 기본적으로 8080번 포트를 사용한다. 22 | 23 | try (ServerSocket listenSocket = new ServerSocket(port)) { 24 | log.info("Web Application Server started {} port.", port); 25 | 26 | // 클라이언트가 연결될때까지 대기한다. 27 | Socket connection; 28 | while ((connection = listenSocket.accept()) != null) { 29 | RequestHandler requestHandler = new RequestHandler(connection); 30 | requestHandler.start(); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} [%-5level] [%thread] [%logger{36}] - %m%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/java/util/HttpRequestUtilsTest.java: -------------------------------------------------------------------------------- 1 | package util; 2 | 3 | import static org.hamcrest.CoreMatchers.*; 4 | import static org.junit.Assert.*; 5 | 6 | import java.util.Map; 7 | 8 | import org.junit.Test; 9 | 10 | import util.HttpRequestUtils.Pair; 11 | 12 | public class HttpRequestUtilsTest { 13 | @Test 14 | public void parseQueryString() { 15 | String queryString = "userId=javajigi"; 16 | Map parameters = HttpRequestUtils.parseQueryString(queryString); 17 | assertThat(parameters.get("userId"), is("javajigi")); 18 | assertThat(parameters.get("password"), is(nullValue())); 19 | 20 | queryString = "userId=javajigi&password=password2"; 21 | parameters = HttpRequestUtils.parseQueryString(queryString); 22 | assertThat(parameters.get("userId"), is("javajigi")); 23 | assertThat(parameters.get("password"), is("password2")); 24 | } 25 | 26 | @Test 27 | public void parseQueryString_null() { 28 | Map parameters = HttpRequestUtils.parseQueryString(null); 29 | assertThat(parameters.isEmpty(), is(true)); 30 | 31 | parameters = HttpRequestUtils.parseQueryString(""); 32 | assertThat(parameters.isEmpty(), is(true)); 33 | 34 | parameters = HttpRequestUtils.parseQueryString(" "); 35 | assertThat(parameters.isEmpty(), is(true)); 36 | } 37 | 38 | @Test 39 | public void parseQueryString_invalid() { 40 | String queryString = "userId=javajigi&password"; 41 | Map parameters = HttpRequestUtils.parseQueryString(queryString); 42 | assertThat(parameters.get("userId"), is("javajigi")); 43 | assertThat(parameters.get("password"), is(nullValue())); 44 | } 45 | 46 | @Test 47 | public void parseCookies() { 48 | String cookies = "logined=true; JSessionId=1234"; 49 | Map parameters = HttpRequestUtils.parseCookies(cookies); 50 | assertThat(parameters.get("logined"), is("true")); 51 | assertThat(parameters.get("JSessionId"), is("1234")); 52 | assertThat(parameters.get("session"), is(nullValue())); 53 | } 54 | 55 | @Test 56 | public void getKeyValue() throws Exception { 57 | Pair pair = HttpRequestUtils.getKeyValue("userId=javajigi", "="); 58 | assertThat(pair, is(new Pair("userId", "javajigi"))); 59 | } 60 | 61 | @Test 62 | public void getKeyValue_invalid() throws Exception { 63 | Pair pair = HttpRequestUtils.getKeyValue("userId", "="); 64 | assertThat(pair, is(nullValue())); 65 | } 66 | 67 | @Test 68 | public void parseHeader() throws Exception { 69 | String header = "Content-Length: 59"; 70 | Pair pair = HttpRequestUtils.parseHeader(header); 71 | assertThat(pair, is(new Pair("Content-Length", "59"))); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/util/IOUtilsTest.java: -------------------------------------------------------------------------------- 1 | package util; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.StringReader; 5 | 6 | import org.junit.Test; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | public class IOUtilsTest { 11 | private static final Logger logger = LoggerFactory.getLogger(IOUtilsTest.class); 12 | 13 | @Test 14 | public void readData() throws Exception { 15 | String data = "abcd123"; 16 | StringReader sr = new StringReader(data); 17 | BufferedReader br = new BufferedReader(sr); 18 | 19 | logger.debug("parse body : {}", IOUtils.readData(br, data.length())); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webapp/css/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | A custom Bootstrap 3.2 'Google Plus style' theme 3 | from http://bootply.com 4 | 5 | This CSS code should follow the 'bootstrap.css' 6 | in your HTML file. 7 | 8 | license: MIT 9 | author: bootply.com 10 | */ 11 | 12 | @import url(http://fonts.googleapis.com/css?family=Roboto:400); 13 | body { 14 | background-color:#e0e0e0; 15 | -webkit-font-smoothing: antialiased; 16 | font: normal 14px Roboto,arial,sans-serif; 17 | } 18 | .navbar-default {background-color:#f4f4f4;margin-top:50px;border-width:0;z-index:5;} 19 | .navbar-default .navbar-nav > .active > a,.navbar-default .navbar-nav > li:hover > a {border:0 solid #4285f4;border-bottom-width:2px;font-weight:800;background-color:transparent;} 20 | .navbar-default .dropdown-menu {background-color:#ffffff;} 21 | .navbar-default .dropdown-menu li > a {padding-left:30px;} 22 | 23 | .header {background-color:#ffffff;border-width:0;} 24 | .header .navbar-collapse {background-color:#ffffff;} 25 | .btn,.form-control,.panel,.list-group,.well {border-radius:1px;box-shadow:0 0 0;} 26 | .form-control {border-color:#d7d7d7;} 27 | .btn-primary {border-color:transparent;} 28 | .btn-primary,.label-primary,.list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus {background-color:#4285f4;} 29 | .btn-plus {background-color:#ffffff;border-width:1px;border-color:#dddddd;box-shadow:1px 1px 0 #999999;border-radius:3px;color:#666666;text-shadow:0 0 1px #bbbbbb;} 30 | .well,.panel {border-color:#d2d2d2;box-shadow:0 1px 0 #cfcfcf;border-radius:3px;} 31 | .btn-success,.label-success,.progress-bar-success{background-color:#65b045;} 32 | .btn-info,.label-info,.progress-bar-info{background-color:#a0c3ff;border-color:#a0c3ff;} 33 | .btn-danger,.label-danger,.progress-bar-danger{background-color:#dd4b39;} 34 | .btn-warning,.label-warning,.progress-bar-warning{background-color:#f4b400;color:#444444;} 35 | 36 | hr {border-color:#ececec;} 37 | button { 38 | outline: 0; 39 | } 40 | textarea { 41 | resize: none; 42 | outline: 0; 43 | } 44 | .panel .btn i,.btn span{ 45 | color:#666666; 46 | } 47 | .panel .panel-heading { 48 | background-color:#ffffff; 49 | font-weight:700; 50 | font-size:16px; 51 | color:#262626; 52 | border-color:#ffffff; 53 | } 54 | .panel .panel-heading a { 55 | font-weight:400; 56 | font-size:11px; 57 | } 58 | .panel .panel-default { 59 | border-color:#cccccc; 60 | } 61 | .panel .panel-thumbnail { 62 | padding:0; 63 | } 64 | .panel .img-circle { 65 | width:50px; 66 | height:50px; 67 | } 68 | .list-group-item:first-child,.list-group-item:last-child { 69 | border-radius:0; 70 | } 71 | h3,h4,h5 { 72 | border:0 solid #efefef; 73 | border-bottom-width:1px; 74 | padding-bottom:10px; 75 | } 76 | .modal-dialog { 77 | width: 450px; 78 | } 79 | .modal-footer { 80 | border-width:0; 81 | } 82 | .dropdown-menu { 83 | background-color:#f4f4f4; 84 | border-color:#f0f0f0; 85 | border-radius:0; 86 | margin-top:-1px; 87 | } 88 | /* end theme */ 89 | 90 | /* template layout*/ 91 | #subnav { 92 | position:fixed; 93 | width:100%; 94 | } 95 | 96 | @media (max-width: 768px) { 97 | #subnav { 98 | padding-top: 6px; 99 | } 100 | } 101 | 102 | #main { 103 | padding-top:120px; 104 | } 105 | 106 | .content-main { 107 | position:relative; 108 | padding:1em; 109 | padding-right: 1em; 110 | border-bottom:1px 111 | } 112 | .qna-header { 113 | position:relative;padding:1em 114 | } 115 | 116 | .article { 117 | position:relative;padding:.6em 0 .8em 118 | } 119 | 120 | .article-header{ 121 | display:table;width:100%; 122 | padding-bottom:.6em; 123 | border-bottom:1px dashed #dadada 124 | } 125 | .article-header-text,.article-header-thumb{display:table-cell;vertical-align:middle} 126 | .article-header-thumb{width:48px} 127 | .article-author-thumb{width:40px;height:40px;-webkit-border-radius:3px;border-radius:3px} 128 | .article-author-name{font-weight:bold;font-size:1.1em} 129 | .article-author-fbgroup{display:inline-block;margin-left:.8em;font-style:italic;font-size:1em;color:#666;} 130 | .article-author-fbgroup a{display:inline-block;color:#333;font-weight:bold;} 131 | @media (max-width: 480px){.article-author-fbgroup a{overflow:hidden;max-width:8em;white-space:nowrap;-o-text-overflow:ellipsis;text-overflow:ellipsis} 132 | }.article-header-time{display:block;font-size:.8em;color:#666;} 133 | .article-header-time:hover{text-decoration:none;} 134 | .article-header-time:hover .icon-link{color:#3aa} 135 | .article-doc,.article-util{padding-left:48px;} 136 | @media (max-width: 480px){.article-doc,.article-util,.article-doc,.article-util{padding-left:0} 137 | }.article-doc img{max-width:100%} 138 | .article-doc blockquote{margin-left:0;margin-right:0;border-left:4px solid #efefef;padding-left:.5em} 139 | .article-util form{display:inline-block} 140 | .article-util-list a,.article-util-list button{color:#999;padding:.4em;-webkit-border-radius:3px;border-radius:3px;border:0;background-color:transparent;white-space:nowrap;} 141 | .article-util-list a:hover,.article-util-list button:hover{text-decoration:none;background-color:#dadada} 142 | .article-util-list{margin:0;padding:0;list-style:none;color:#999;} 143 | .article-util-list li{display:inline-block;} 144 | .article-util-list li:before{content:'•';margin:0 -.4em} 145 | .article-util-list li:first-child:before{display:none} 146 | .article-util-list .btn-like-article{color:#44ca7f;} 147 | .article-util-list .btn-like-article:hover{background-color:#e3f7ec} 148 | .article-util-list .btn-dislike-article{color:#be6060;} 149 | .article-util-list .btn-dislike-article:hover{background-color:#f9f1f1} 150 | .article-util-list .link-answer-article{color:#5989b6;} 151 | .article-util-list .link-answer-article:hover{background-color:#e7eef4} 152 | .article-util-list .foundicon-thumb-up,.article-util-list .foundicon-thumb-down{position:relative;top:2px}.article-util form{display:inline-block} 153 | 154 | .qna-list h1 small { 155 | font-size:.4em 156 | } 157 | .qna-list .list { 158 | list-style:none; 159 | margin:0; 160 | padding:0; 161 | border-top:1px solid #dadada; 162 | font-size:14px; 163 | } 164 | 165 | @media (max-width: 768px) { 166 | .qna-list .list{ margin:0 -1em } 167 | } 168 | 169 | .qna-list .list > li { 170 | position:relative; 171 | padding:1em; 172 | padding-left:4em; 173 | border-bottom:1px solid #eaeaea 174 | } 175 | .qna-list .list .main .subject{display:block;font-size:1.2em;line-height:1.4em;} 176 | .qna-list .list .main .subject a{display:block;color:#000} 177 | .qna-list .list .tags{font-size:.9em;color:#999} 178 | .qna-list .list .tag:before{content:', '} 179 | .qna-list .list .tag:first-child:before{content:''} 180 | .qna-list .list .auth-info{margin-top:.8em;color:#999;} 181 | .qna-list .list .auth-info .type{padding-right:.4em;} 182 | @media (max-width: 480px){.qna-list .list .auth-info .type{display:none} 183 | }.qna-list .list .auth-info .time{font-size:.9em} 184 | .qna-list .list .reply{position:absolute;left:0;top:.8em;width:4em;text-align:center;} 185 | .qna-list .list .reply .icon-reply{position:absolute;left:0;top:0;font-size:2em;color:#eee;} 186 | @media (max-width: 979px){.qna-list .list .reply .icon-reply{left:.5em} 187 | }.qna-list .list .reply .point{position:relative;z-index:1;color:#aaa;font-size:2em;line-height:2em;letter-spacing:-.2em} 188 | .qna-header{border-bottom:4px solid #efefef} 189 | .qna-title{margin-bottom:0} 190 | .qna-nav{padding-bottom:.67em;} 191 | .qna-nav .divider{padding-left:.4em;padding-right:.4em;font-size:.8em;color:#dadada} 192 | .qna-comment .article{border-bottom:1px solid #dfdfdf;} 193 | .qna-comment .article:first-child{border-top:1px solid #dfdfdf} 194 | .qna-comment .article.best-article{border-bottom-width:4px;} 195 | .qna-comment .article.best-article .btn-like-article{top:108px} 196 | .qna-comment-slipp,.qna-comment-fb{margin-top:2em;border-top:4px solid #efefef} 197 | .qna-comment-count{margin:0;padding:1em 0 0;font-weight:bold;font-size:1.2em} 198 | 199 | .qna-write { 200 | padding-top: 1.4em; 201 | padding-right: 2.5em; 202 | } 203 | 204 | -------------------------------------------------------------------------------- /webapp/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slipp/web-application-server/dd0ab721fc346a3a8d4d2c6a19d82e180cc953fe/webapp/favicon.ico -------------------------------------------------------------------------------- /webapp/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slipp/web-application-server/dd0ab721fc346a3a8d4d2c6a19d82e180cc953fe/webapp/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /webapp/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slipp/web-application-server/dd0ab721fc346a3a8d4d2c6a19d82e180cc953fe/webapp/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /webapp/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slipp/web-application-server/dd0ab721fc346a3a8d4d2c6a19d82e180cc953fe/webapp/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /webapp/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slipp/web-application-server/dd0ab721fc346a3a8d4d2c6a19d82e180cc953fe/webapp/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /webapp/images/80-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slipp/web-application-server/dd0ab721fc346a3a8d4d2c6a19d82e180cc953fe/webapp/images/80-text.png -------------------------------------------------------------------------------- /webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SLiPP Java Web Programming 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 48 | 80 | 81 |
82 |
83 |
84 | 122 |
123 |
124 |
125 |
    126 |
  • «
  • 127 |
  • 1
  • 128 |
  • 2
  • 129 |
  • 3
  • 130 |
  • 4
  • 131 |
  • 5
  • 132 |
  • »
  • 133 |
134 |
135 |
136 | 질문하기 137 |
138 |
139 |
140 |
141 |
142 | 143 | 144 | 177 | 178 | 179 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /webapp/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.2.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.2.0",d.prototype.close=function(b){function c(){f.detach().trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one("bsTransitionEnd",c).emulateTransitionEnd(150):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.2.0",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),d[e](null==f[b]?this.options[b]:f[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b).on("keydown.bs.carousel",a.proxy(this.keydown,this)),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.2.0",c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},c.prototype.keydown=function(a){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.to=function(b){var c=this,d=this.getItemIndex(this.$active=this.$element.find(".item.active"));return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=e[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:g});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,f&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(e)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:g});return a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one("bsTransitionEnd",function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger(m)),f&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(b=!b),e||d.data("bs.collapse",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};c.VERSION="3.2.0",c.DEFAULTS={toggle:!0},c.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},c.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var c=a.Event("show.bs.collapse");if(this.$element.trigger(c),!c.isDefaultPrevented()){var d=this.$parent&&this.$parent.find("> .panel > .in");if(d&&d.length){var e=d.data("bs.collapse");if(e&&e.transitioning)return;b.call(d,"hide"),e||d.data("bs.collapse",null)}var f=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[f](0),this.transitioning=1;var g=function(){this.$element.removeClass("collapsing").addClass("collapse in")[f](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return g.call(this);var h=a.camelCase(["scroll",f].join("-"));this.$element.one("bsTransitionEnd",a.proxy(g,this)).emulateTransitionEnd(350)[f](this.$element[0][h])}}},c.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},c.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var d=a.fn.collapse;a.fn.collapse=b,a.fn.collapse.Constructor=c,a.fn.collapse.noConflict=function(){return a.fn.collapse=d,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(c){var d,e=a(this),f=e.attr("data-target")||c.preventDefault()||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),g=a(f),h=g.data("bs.collapse"),i=h?"toggle":e.data(),j=e.attr("data-parent"),k=j&&a(j);h&&h.transitioning||(k&&k.find('[data-toggle="collapse"][data-parent="'+j+'"]').not(e).addClass("collapsed"),e[g.hasClass("in")?"addClass":"removeClass"]("collapsed")),b.call(g,i)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.2.0",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('