diff --git a/README.md b/README.md index efa0dec..8ebd4ff 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,6 @@

- - ## ๐Ÿ“ ๊ณตํ†ต ์š”๊ตฌ์‚ฌํ•ญ 1. ์ „์ฒด ๋ฏธ์…˜์€ 7๋‹จ๊ณ„๋กœ ๋‚˜๋‰˜์–ด์ ธ ์žˆ์œผ๋ฉฐ, ๊ฐ Step์—๋Š” `ํ•„์ˆ˜/์„ ํƒ ๊ตฌํ˜„ ์‚ฌํ•ญ`, `ํ•™์Šต ๋ชฉํ‘œ`๊ฐ€ ์ฃผ์–ด์ง‘๋‹ˆ๋‹ค. @@ -36,17 +34,42 @@ ๋„คํŠธ์›Œํฌ๋กœ ๋ถ€ํ„ฐ ์ „์†ก๋œ ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์‹ฑํ•ด ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ €์žฅํ•œ๋‹ค. -- [ ] ์ •์  ํŽ˜์ด์ง€๋ฅผ ํ™”๋ฉด์— ๋„์šด๋‹ค. -- [ ] ํ”„๋ก ํŠธ ์ปจํŠธ๋กค๋Ÿฌ ํŒจํ„ด์„ ์ ์šฉํ•œ๋‹ค. -- [ ] ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด๋ถ€ ์ธ๋ฉ”๋ชจ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. -- [ ] ์–ด๋–ค ์ •๋ณด๋ฅผ ์ €์žฅํ•  ์ง€๋Š” ์ž์œ ๋กญ๊ฒŒ ์ •์˜ํ•œ๋‹ค. +- [x] ์ •์  ํŽ˜์ด์ง€๋ฅผ ํ™”๋ฉด์— ๋„์šด๋‹ค. +- [x] ํ”„๋ก ํŠธ ์ปจํŠธ๋กค๋Ÿฌ ํŒจํ„ด์„ ์ ์šฉํ•œ๋‹ค. +- [x] ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด๋ถ€ ์ธ๋ฉ”๋ชจ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. +- [x] ์–ด๋–ค ์ •๋ณด๋ฅผ ์ €์žฅํ•  ์ง€๋Š” ์ž์œ ๋กญ๊ฒŒ ์ •์˜ํ•œ๋‹ค.

### ํ•™์Šต ๋ชฉํ‘œ +1. ๋„คํŠธ์›Œํฌ๋กœ ๋ถ€ํ„ฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊นŒ์ง€ ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋–ป๊ฒŒ ์ „์†ก๋˜๋Š”์ง€ ํ•™์Šตํ•œ๋‹ค. +2. DispatcherServlet, ํ”„๋ก ํŠธ ์ปจํŠธ๋กค๋Ÿฌ ํŒจํ„ด์˜ ๊ฐœ๋…๊ณผ ๋™์ž‘ ์›๋ฆฌ๋ฅผ ํ•™์Šตํ•œ๋‹ค. -- [ ] ๋„คํŠธ์›Œํฌ๋กœ ๋ถ€ํ„ฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊นŒ์ง€ ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋–ป๊ฒŒ ์ „์†ก๋˜๋Š”์ง€ ํ•™์Šตํ•œ๋‹ค. -- [ ] DispatcherServlet, ํ”„๋ก ํŠธ ์ปจํŠธ๋กค๋Ÿฌ ํŒจํ„ด์˜ ๊ฐœ๋…๊ณผ ๋™์ž‘ ์›๋ฆฌ๋ฅผ ํ•™์Šตํ•œ๋‹ค. -- [ ] DispatcherServlet์˜ ๋™์ž‘ ์›๋ฆฌ๋ฅผ ์ดํ•ดํ•œ๋‹ค. +
+
+
+
+
+
+## Step2. ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ๋‹ค. + +์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ๋‹ค. + +1. ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ๋‹ค. +- ์„ธ์…˜์„ ์ด์šฉํ•ด ๊ตฌํ˜„ํ•œ๋‹ค. +- ์„ธ์…˜์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด๋ถ€์— ์ €์žฅ/๊ด€๋ฆฌํ•œ๋‹ค. + - ์„ธ์…˜ ์œ ์ง€ ์‹œ๊ฐ„์„ ์ œํ•œ ํ•œ๋‹ค. + - [์„ ํƒ] ์ตœ๊ทผ ๋กœ๊ทธ์ธ ๊ธฐ๋ก๊ณผ ์•„์ดํ”ผ๋ฅผ ์‹๋ณ„ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค. + +2. ๊ฐœ์ธ ์ •๋ณด ์ƒ์„ธ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ๊ฐœ๋ฐœํ•œ๋‹ค. +- [์„ ํƒ] ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ๋‹ค. + +
+
+ +### ํ•™์Šต ๋ชฉํ‘œ +- HTTP ํŠน์ง•์— ๋Œ€ํ•ด ํ•™์Šตํ•œ๋‹ค. + - ์ฟ ํ‚ค/์„ธ์…˜์— ๋Œ€ํ•ด ํ•™์Šตํ•œ๋‹ค. + - ์„ธ์…˜ ๊ด€๋ฆฌ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ํ•™์Šตํ•œ๋‹ค. diff --git a/app/build.gradle b/app/build.gradle index b2c186f..4bde625 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,3 +1,70 @@ +plugins { + id("org.sonarqube") version("4.2.1.3168") + id("jacoco") +} + dependencies { implementation(project(":mvc")) } + +tasks.named("test") { + useJUnitPlatform() + finalizedBy jacocoTestReport +} + +jacoco { + toolVersion = project.getProperty("jacocoVersion") +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + csv.required = false + html.required = true + xml.destination file("${buildDir}/jacoco/index.xml") + csv.destination file("${buildDir}/jacoco/index.csv") + html.destination file("${buildDir}/jacoco/index.html") + } + + afterEvaluate { + classDirectories.setFrom( + files(classDirectories.files.collect { + fileTree(dir: it, excludes: []) + }) + ) + } + finalizedBy("jacocoTestCoverageVerification") +} + +jacocoTestCoverageVerification { + violationRules { + rule { + enabled = true + element = "CLASS" + limit { + counter = "BRANCH" + value = "COVEREDRATIO" + minimum = 0.50 + } + } + } +} + +sonarqube { + properties { + property("sonar.host.url", System.getenv("SONAR_QUBE_SERVER_URL")) + property("sonar.login", System.getenv("SONAR_QUBE_TOKEN")) + property("sonar.sources", "src/main/java") + property("sonar.tests", "src/test/java") + property("sonar.language", "java") + property("sonar.projectKey", System.getenv("SONAR_PROJECT_KEY")) + property("sonar.projectName", System.getenv("SONAR_PROJECT_NAME")) + property("sonar.java.source", 17) + property("sonar.sourceEncoding", "UTF-8") + property("sonar.java.binaries", "${buildDir}/classes") + property("sonar.test.inclusions", "") + property("sonar.exclusions", "") + property("sonar.coverage.jacoco.xmlReportPaths", "${buildDir}/jacoco/index.xml") + } +} diff --git a/app/src/main/java/project/server/app/BlogApplication.java b/app/src/main/java/project/server/app/BlogApplication.java index 60f8631..f126a7c 100644 --- a/app/src/main/java/project/server/app/BlogApplication.java +++ b/app/src/main/java/project/server/app/BlogApplication.java @@ -1,10 +1,15 @@ package project.server.app; -import project.server.mvc.springframework.context.Application; +import project.server.mvc.Application; +import static project.server.mvc.tomcat.PortFinder.findPort; public class BlogApplication { - public static void main(String[] args) throws Exception { - Application.run(args); + public static void main(String[] args) { + String packages = "project"; + final int port = findPort(args); + final int tomcatThreadCount = 200; + Application application = new Application(packages, port, tomcatThreadCount); + application.start(); } } diff --git a/app/src/main/java/project/server/app/common/codeandmessage/failure/ErrorCodeAndMessages.java b/app/src/main/java/project/server/app/common/codeandmessage/failure/ErrorCodeAndMessages.java index dd1df1b..e4c1e7c 100644 --- a/app/src/main/java/project/server/app/common/codeandmessage/failure/ErrorCodeAndMessages.java +++ b/app/src/main/java/project/server/app/common/codeandmessage/failure/ErrorCodeAndMessages.java @@ -4,8 +4,10 @@ import project.server.mvc.servlet.http.HttpStatus; public enum ErrorCodeAndMessages implements ErrorCodeAndMessage { + INVALID_SESSION(HttpStatus.UN_AUTHORIZED, "์„ธ์…˜์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."), INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "์˜ฌ๋ฐ”๋ฅธ ๊ฐ’์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."), - PAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "ํŽ˜์ด์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + PAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "ํŽ˜์ด์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + UN_AUTHORIZED(HttpStatus.UN_AUTHORIZED, "๊ถŒํ•œ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); private final HttpStatus httpStatus; private final String errorMessage; diff --git a/app/src/main/java/project/server/app/common/exception/SessionExpiredException.java b/app/src/main/java/project/server/app/common/exception/SessionExpiredException.java new file mode 100644 index 0000000..ac4a485 --- /dev/null +++ b/app/src/main/java/project/server/app/common/exception/SessionExpiredException.java @@ -0,0 +1,10 @@ +package project.server.app.common.exception; + +import static project.server.app.common.codeandmessage.failure.ErrorCodeAndMessages.INVALID_SESSION; + +public class SessionExpiredException extends RuntimeException { + + public SessionExpiredException() { + super(INVALID_SESSION.getErrorMessage()); + } +} diff --git a/app/src/main/java/project/server/app/common/exception/UnAuthorizedException.java b/app/src/main/java/project/server/app/common/exception/UnAuthorizedException.java new file mode 100644 index 0000000..9b0981b --- /dev/null +++ b/app/src/main/java/project/server/app/common/exception/UnAuthorizedException.java @@ -0,0 +1,10 @@ +package project.server.app.common.exception; + +import static project.server.app.common.codeandmessage.failure.ErrorCodeAndMessages.UN_AUTHORIZED; + +public class UnAuthorizedException extends RuntimeException { + + public UnAuthorizedException() { + super(UN_AUTHORIZED.getErrorMessage()); + } +} diff --git a/app/src/main/java/project/server/app/common/login/LoginUser.java b/app/src/main/java/project/server/app/common/login/LoginUser.java new file mode 100644 index 0000000..2bd1034 --- /dev/null +++ b/app/src/main/java/project/server/app/common/login/LoginUser.java @@ -0,0 +1,59 @@ +package project.server.app.common.login; + +import static java.time.LocalDateTime.now; +import java.util.Objects; + +public class LoginUser { + + private final Long userId; + private final String loginIp; + private boolean valid; + + public LoginUser( + Long userId, + String loginIp + ) { + this.userId = userId; + this.loginIp = loginIp; + } + + public LoginUser(Session session) { + this.userId = session.userId(); + this.loginIp = null; + this.valid = session.isValid(now()); + } + + public Long getUserId() { + return userId; + } + + public String getLoginIp() { + return loginIp; + } + + public boolean isValid() { + return valid; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + LoginUser loginUser = (LoginUser) object; + return userId.equals(loginUser.userId); + } + + @Override + public int hashCode() { + return Objects.hash(userId); + } + + @Override + public String toString() { + return String.format("userId:%s", userId); + } +} diff --git a/app/src/main/java/project/server/app/common/login/Session.java b/app/src/main/java/project/server/app/common/login/Session.java new file mode 100644 index 0000000..7d01971 --- /dev/null +++ b/app/src/main/java/project/server/app/common/login/Session.java @@ -0,0 +1,41 @@ +package project.server.app.common.login; + +import java.time.LocalDateTime; +import java.util.Objects; + +public record Session( + Long userId, + String sessionId, + LocalDateTime expiredAt +) { + + public boolean isValid(LocalDateTime expiredAt) { + return this.expiredAt.isAfter(expiredAt); + } + + public String getUserIdAsString() { + return userId.toString(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + Session session = (Session) object; + return sessionId.equals(session.sessionId); + } + + @Override + public int hashCode() { + return Objects.hash(sessionId); + } + + @Override + public String toString() { + return String.format("userId:%s, sessionId:%s, expiredAt:%s", userId, sessionId, expiredAt); + } +} diff --git a/app/src/main/java/project/server/app/common/login/SessionStore.java b/app/src/main/java/project/server/app/common/login/SessionStore.java new file mode 100644 index 0000000..3c1da5f --- /dev/null +++ b/app/src/main/java/project/server/app/common/login/SessionStore.java @@ -0,0 +1,34 @@ +package project.server.app.common.login; + +import java.time.LocalDateTime; +import static java.time.LocalDateTime.now; +import java.util.Map; +import java.util.Optional; +import static java.util.UUID.randomUUID; +import java.util.concurrent.ConcurrentHashMap; +import lombok.extern.slf4j.Slf4j; +import project.server.app.core.web.user.application.SessionManager; +import project.server.mvc.springframework.annotation.Component; + +@Slf4j +@Component +public class SessionStore implements SessionManager { + + private static final int FIFTEEN_MINUTES = 15; + private static final Map factory = new ConcurrentHashMap<>(); + + @Override + public Session createSession(Long userId) { + String uuid = randomUUID().toString(); + LocalDateTime after15Minutes = now().plusMinutes(FIFTEEN_MINUTES); + + Session newSession = new Session(userId, uuid, after15Minutes); + factory.put(userId, newSession); + return newSession; + } + + @Override + public Optional findByUserId(Long userId) { + return Optional.ofNullable(factory.get(userId)); + } +} diff --git a/app/src/main/java/project/server/app/common/utils/HeaderUtils.java b/app/src/main/java/project/server/app/common/utils/HeaderUtils.java new file mode 100644 index 0000000..779e571 --- /dev/null +++ b/app/src/main/java/project/server/app/common/utils/HeaderUtils.java @@ -0,0 +1,39 @@ +package project.server.app.common.utils; + +import java.util.Map; +import project.server.mvc.servlet.http.Cookie; +import project.server.mvc.servlet.http.Cookies; + +public final class HeaderUtils { + + private static final String SESSION_ID = "sessionId"; + + private HeaderUtils() { + throw new AssertionError("์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ์‹์œผ๋กœ ์ƒ์„ฑ์ž๋ฅผ ํ˜ธ์ถœํ•ด์ฃผ์„ธ์š”."); + } + + public static Long getSessionId(Cookies cookies) { + Map cookiesMap = cookies.getCookiesMap(); + Cookie findCookie = cookiesMap.get(SESSION_ID); + if (findCookie != null) { + return extractSessionId(findCookie); + } + return null; + } + + private static Long extractSessionId(Cookie cookie) { + if (cookie.value() != null) { + String sessionId = cookie.value(); + return parseLong(sessionId); + } + return null; + } + + private static Long parseLong(String sessionId) { + try { + return Long.valueOf(sessionId); + } catch (NumberFormatException exception) { + return null; + } + } +} diff --git a/app/src/main/java/project/server/app/core/domain/user/Deleted.java b/app/src/main/java/project/server/app/core/domain/user/Deleted.java new file mode 100644 index 0000000..bd2d4a2 --- /dev/null +++ b/app/src/main/java/project/server/app/core/domain/user/Deleted.java @@ -0,0 +1,6 @@ +package project.server.app.core.domain.user; + +public enum Deleted { + TRUE, + FALSE +} diff --git a/app/src/main/java/project/server/app/core/domain/user/User.java b/app/src/main/java/project/server/app/core/domain/user/User.java index cb3dc3c..4a2c59d 100644 --- a/app/src/main/java/project/server/app/core/domain/user/User.java +++ b/app/src/main/java/project/server/app/core/domain/user/User.java @@ -1,12 +1,17 @@ package project.server.app.core.domain.user; +import java.time.LocalDateTime; import java.util.Objects; +import static project.server.app.core.domain.user.Deleted.FALSE; public class User { private Long id; private Username username; private Password password; + private LocalDateTime createdAt; + private LocalDateTime lastModifiedAt; + private Deleted deleted; public User( String username, @@ -17,12 +22,26 @@ public User( public User( Long id, - String name, + String username, String password + ) { + this(id, username, password, LocalDateTime.now(), null, FALSE); + } + + public User( + Long id, + String username, + String password, + LocalDateTime createdAt, + LocalDateTime lastModifiedAt, + Deleted deleted ) { this.id = id; - this.username = new Username(name); + this.username = new Username(username); this.password = new Password(password); + this.createdAt = createdAt; + this.lastModifiedAt = lastModifiedAt; + this.deleted = deleted; } public Long getId() { @@ -37,8 +56,12 @@ public Username getUsernameAsValue() { return username; } - public Password getPassword() { - return password; + public String getPassword() { + return password.value(); + } + + public Deleted getDeleted() { + return deleted; } public boolean isNew() { @@ -49,6 +72,10 @@ public void registerId(Long id) { this.id = id; } + public boolean isAlreadyDeleted() { + return this.deleted.equals(Deleted.TRUE); + } + @Override public boolean equals(Object object) { if (this == object) { @@ -60,6 +87,11 @@ public boolean equals(Object object) { return getId().equals(user.getId()); } + public void delete(LocalDateTime lastModifiedAt) { + this.lastModifiedAt = lastModifiedAt; + this.deleted = Deleted.TRUE; + } + @Override public int hashCode() { return Objects.hash(getId()); diff --git a/app/src/main/java/project/server/app/core/domain/user/UserRepository.java b/app/src/main/java/project/server/app/core/domain/user/UserRepository.java index d503e8d..1bf5f38 100644 --- a/app/src/main/java/project/server/app/core/domain/user/UserRepository.java +++ b/app/src/main/java/project/server/app/core/domain/user/UserRepository.java @@ -13,4 +13,6 @@ public interface UserRepository { boolean existByName(String username); List findAll(); + + Optional findByUsernameAndPassword(String username, String password); } diff --git a/app/src/main/java/project/server/app/core/web/user/application/SessionManager.java b/app/src/main/java/project/server/app/core/web/user/application/SessionManager.java new file mode 100644 index 0000000..5a17e11 --- /dev/null +++ b/app/src/main/java/project/server/app/core/web/user/application/SessionManager.java @@ -0,0 +1,10 @@ +package project.server.app.core.web.user.application; + +import java.util.Optional; +import project.server.app.common.login.Session; + +public interface SessionManager { + Session createSession(Long userId); + + Optional findByUserId(Long userId); +} diff --git a/app/src/main/java/project/server/app/core/web/user/application/UserDeleteUseCase.java b/app/src/main/java/project/server/app/core/web/user/application/UserDeleteUseCase.java new file mode 100644 index 0000000..6c072ad --- /dev/null +++ b/app/src/main/java/project/server/app/core/web/user/application/UserDeleteUseCase.java @@ -0,0 +1,7 @@ +package project.server.app.core.web.user.application; + +import project.server.app.common.login.LoginUser; + +public interface UserDeleteUseCase { + void delete(LoginUser loginUser); +} diff --git a/app/src/main/java/project/server/app/core/web/user/application/UserLoginUseCase.java b/app/src/main/java/project/server/app/core/web/user/application/UserLoginUseCase.java new file mode 100644 index 0000000..73cfeb3 --- /dev/null +++ b/app/src/main/java/project/server/app/core/web/user/application/UserLoginUseCase.java @@ -0,0 +1,9 @@ +package project.server.app.core.web.user.application; + +import project.server.app.common.login.Session; + +public interface UserLoginUseCase { + Session login(String username, String password); + + Session findSessionById(Long userId); +} diff --git a/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginService.java b/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginService.java new file mode 100644 index 0000000..76b1eea --- /dev/null +++ b/app/src/main/java/project/server/app/core/web/user/application/service/UserLoginService.java @@ -0,0 +1,50 @@ +package project.server.app.core.web.user.application.service; + +import static java.time.LocalDateTime.now; +import lombok.extern.slf4j.Slf4j; +import project.server.app.common.exception.SessionExpiredException; +import project.server.app.common.exception.UnAuthorizedException; +import project.server.app.common.login.Session; +import project.server.app.core.domain.user.User; +import project.server.app.core.domain.user.UserRepository; +import project.server.app.core.web.user.application.SessionManager; +import project.server.app.core.web.user.application.UserLoginUseCase; +import project.server.app.core.web.user.exception.UserNotFoundException; +import project.server.mvc.springframework.annotation.Service; + +@Slf4j +@Service +public class UserLoginService implements UserLoginUseCase { + + private final SessionManager sessionManager; + private final UserRepository userRepository; + + public UserLoginService( + SessionManager sessionManager, + UserRepository userRepository + ) { + this.sessionManager = sessionManager; + this.userRepository = userRepository; + } + + @Override + public Session login( + String username, + String password + ) { + User findUser = userRepository.findByUsernameAndPassword(username, password) + .orElseThrow(UserNotFoundException::new); + return sessionManager.createSession(findUser.getId()); + } + + @Override + public Session findSessionById(Long userId) { + Session findSession = sessionManager.findByUserId(userId) + .orElseThrow(UnAuthorizedException::new); + + if (!findSession.isValid(now())) { + throw new SessionExpiredException(); + } + return findSession; + } +} diff --git a/app/src/main/java/project/server/app/core/web/user/application/service/UserService.java b/app/src/main/java/project/server/app/core/web/user/application/service/UserService.java index 7df3331..38e9e2a 100644 --- a/app/src/main/java/project/server/app/core/web/user/application/service/UserService.java +++ b/app/src/main/java/project/server/app/core/web/user/application/service/UserService.java @@ -1,7 +1,10 @@ package project.server.app.core.web.user.application.service; +import java.time.LocalDateTime; +import project.server.app.common.login.LoginUser; import project.server.app.core.domain.user.User; import project.server.app.core.domain.user.UserRepository; +import project.server.app.core.web.user.application.UserDeleteUseCase; import project.server.app.core.web.user.application.UserSaveUseCase; import project.server.app.core.web.user.application.UserSearchUseCase; import project.server.app.core.web.user.exception.DuplicatedUsernameException; @@ -9,7 +12,7 @@ import project.server.mvc.springframework.annotation.Service; @Service -public class UserService implements UserSaveUseCase, UserSearchUseCase { +public class UserService implements UserSaveUseCase, UserSearchUseCase, UserDeleteUseCase { private final UserRepository userRepository; @@ -31,4 +34,16 @@ public User findById(Long userId) { return userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); } + + @Override + public void delete(LoginUser loginUser) { + User findUser = userRepository.findById(loginUser.getUserId()) + .orElseThrow(UserNotFoundException::new); + + if (findUser.isAlreadyDeleted()) { + throw new UserNotFoundException(); + } + + findUser.delete(LocalDateTime.now()); + } } diff --git a/app/src/main/java/project/server/app/core/web/user/persistence/UserPersistenceRepository.java b/app/src/main/java/project/server/app/core/web/user/persistence/UserPersistenceRepository.java index b9121a4..a0fa24b 100644 --- a/app/src/main/java/project/server/app/core/web/user/persistence/UserPersistenceRepository.java +++ b/app/src/main/java/project/server/app/core/web/user/persistence/UserPersistenceRepository.java @@ -9,10 +9,10 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; import project.server.app.core.domain.user.User; import project.server.app.core.domain.user.UserRepository; import project.server.app.core.web.user.exception.AlreadyRegisteredUserException; -import static project.server.app.core.web.user.exception.UserErrorCodeAndMessage.ALREADY_SAVED_USER; import project.server.mvc.springframework.annotation.Repository; @Repository @@ -49,7 +49,7 @@ public Optional findById(Long userId) { @Override public boolean existByName(String username) { User findUser = factory.values().stream() - .filter(x -> x.getUsername().equals(username)) + .filter(equalsUsername(username)) .findAny() .orElseGet(() -> null); return findUser != null ? ALREADY_EXIST : NOT_FOUND; @@ -61,8 +61,30 @@ public List findAll() { .toList(); } + @Override + public Optional findByUsernameAndPassword( + String username, + String password + ) { + return Optional.ofNullable( + factory.values().stream() + .filter(equalsUsername(username)) + .filter(equalsPassword(password)) + .findAny() + .orElseGet(() -> null) + ); + } + @Override public void clear() { factory.clear(); } + + private Predicate equalsUsername(String username) { + return user -> user.getUsername().equals(username); + } + + private Predicate equalsPassword(String password) { + return user -> user.getPassword().equals(password); + } } diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/LoginController.java b/app/src/main/java/project/server/app/core/web/user/presentation/LoginController.java new file mode 100644 index 0000000..a08310a --- /dev/null +++ b/app/src/main/java/project/server/app/core/web/user/presentation/LoginController.java @@ -0,0 +1,54 @@ +package project.server.app.core.web.user.presentation; + +import lombok.extern.slf4j.Slf4j; +import project.server.app.common.login.Session; +import project.server.app.core.web.user.application.UserLoginUseCase; +import project.server.app.core.web.user.presentation.validator.UserValidator; +import project.server.mvc.servlet.HttpServletRequest; +import project.server.mvc.servlet.HttpServletResponse; +import project.server.mvc.springframework.annotation.Controller; +import project.server.mvc.springframework.web.servlet.Handler; +import project.server.mvc.springframework.web.servlet.ModelAndView; +import project.server.mvc.servlet.http.Cookie; +import static project.server.mvc.servlet.http.HttpStatus.OK; + +@Slf4j +@Controller +public class LoginController implements Handler { + + private final UserValidator validator; + private final UserLoginUseCase userLoginUseCase; + + public LoginController( + UserValidator validator, + UserLoginUseCase userLoginUseCase + ) { + this.validator = validator; + this.userLoginUseCase = userLoginUseCase; + } + + @Override + public ModelAndView process( + HttpServletRequest request, + HttpServletResponse response + ) { + String username = request.getAttribute("username"); + String password = request.getAttribute("password"); + log.info("username: {}, password: {}", username, password); + + validator.validateLoginInfo(username, password); + Session session = userLoginUseCase.login(username, password); + + setResponse(response, session); + return new ModelAndView("redirect:/index.html"); + } + + private void setResponse( + HttpServletResponse response, + Session session + ) { + Cookie cookie = new Cookie("sessionId", session.getUserIdAsString()); + response.addCookie(cookie); + response.setStatus(OK); + } +} diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/SignUpController.java b/app/src/main/java/project/server/app/core/web/user/presentation/SignUpController.java index db4b667..859e7e4 100644 --- a/app/src/main/java/project/server/app/core/web/user/presentation/SignUpController.java +++ b/app/src/main/java/project/server/app/core/web/user/presentation/SignUpController.java @@ -6,6 +6,7 @@ import project.server.app.core.web.user.presentation.validator.UserValidator; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; +import static project.server.mvc.servlet.http.HttpStatus.OK; import project.server.mvc.springframework.annotation.Controller; import project.server.mvc.springframework.annotation.RequestMapping; import project.server.mvc.springframework.web.servlet.Handler; @@ -36,8 +37,10 @@ public ModelAndView process( String password = request.getAttribute("password"); log.info("username: {}, password: {}", username, password); - validator.validateSignUp(username, password); + validator.validateLoginInfo(username, password); userSaveUseCase.save(new User(username, password)); + + response.setStatus(OK); return new ModelAndView("redirect:/index.html"); } } diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/UserDeleteController.java b/app/src/main/java/project/server/app/core/web/user/presentation/UserDeleteController.java new file mode 100644 index 0000000..9ef517a --- /dev/null +++ b/app/src/main/java/project/server/app/core/web/user/presentation/UserDeleteController.java @@ -0,0 +1,51 @@ +package project.server.app.core.web.user.presentation; + +import lombok.extern.slf4j.Slf4j; +import static project.server.app.common.utils.HeaderUtils.getSessionId; +import project.server.app.common.login.LoginUser; +import project.server.app.common.login.Session; +import project.server.app.core.web.user.application.UserDeleteUseCase; +import project.server.app.core.web.user.application.UserLoginUseCase; +import project.server.app.core.web.user.presentation.validator.UserValidator; +import project.server.mvc.servlet.HttpServletRequest; +import project.server.mvc.servlet.HttpServletResponse; +import static project.server.mvc.servlet.http.HttpStatus.NO_CONTENT; +import project.server.mvc.springframework.annotation.Controller; +import project.server.mvc.springframework.web.servlet.Handler; +import project.server.mvc.springframework.web.servlet.ModelAndView; + +@Slf4j +@Controller +public class UserDeleteController implements Handler { + + private final UserValidator validator; + private final UserLoginUseCase loginUseCase; + private final UserDeleteUseCase userDeleteUseCase; + + public UserDeleteController( + UserValidator validator, + UserLoginUseCase loginUseCase, + UserDeleteUseCase userDeleteUseCase + ) { + this.validator = validator; + this.loginUseCase = loginUseCase; + this.userDeleteUseCase = userDeleteUseCase; + } + + @Override + public ModelAndView process( + HttpServletRequest request, + HttpServletResponse response + ) { + Long sessionId = getSessionId(request.getCookies()); + validator.validateSessionId(sessionId); + + Session findSession = loginUseCase.findSessionById(sessionId); + log.info("Session:{}", findSession); + LoginUser loginUser = new LoginUser(findSession); + + userDeleteUseCase.delete(loginUser); + response.setStatus(NO_CONTENT); + return new ModelAndView("redirect/index.html"); + } +} diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/UserInfoSearchController.java b/app/src/main/java/project/server/app/core/web/user/presentation/UserInfoSearchController.java new file mode 100644 index 0000000..f19db0f --- /dev/null +++ b/app/src/main/java/project/server/app/core/web/user/presentation/UserInfoSearchController.java @@ -0,0 +1,65 @@ +package project.server.app.core.web.user.presentation; + +import lombok.extern.slf4j.Slf4j; +import static project.server.app.common.utils.HeaderUtils.getSessionId; +import project.server.app.common.login.LoginUser; +import project.server.app.common.login.Session; +import project.server.app.core.domain.user.User; +import project.server.app.core.web.user.application.UserLoginUseCase; +import project.server.app.core.web.user.application.UserSearchUseCase; +import project.server.app.core.web.user.presentation.validator.UserValidator; +import project.server.mvc.servlet.HttpServletRequest; +import project.server.mvc.servlet.HttpServletResponse; +import static project.server.mvc.servlet.http.HttpStatus.OK; +import project.server.mvc.springframework.annotation.Controller; +import project.server.mvc.springframework.annotation.GetMapping; +import project.server.mvc.springframework.annotation.RequestMapping; +import project.server.mvc.springframework.ui.ModelMap; +import project.server.mvc.springframework.web.servlet.Handler; +import project.server.mvc.springframework.web.servlet.ModelAndView; + +@Slf4j +@Controller +@RequestMapping("/users") +public class UserInfoSearchController implements Handler { + + private final UserValidator validator; + private final UserLoginUseCase loginUseCase; + private final UserSearchUseCase userSearchUseCase; + + public UserInfoSearchController( + UserValidator validator, + UserLoginUseCase loginUseCase, + UserSearchUseCase userSearchUseCase + ) { + this.validator = validator; + this.loginUseCase = loginUseCase; + this.userSearchUseCase = userSearchUseCase; + } + + @Override + @GetMapping(path = "/users/{userId}") + public ModelAndView process( + HttpServletRequest request, + HttpServletResponse response + ) { + Long sessionId = getSessionId(request.getCookies()); + validator.validateSessionId(sessionId); + + Session findSession = loginUseCase.findSessionById(sessionId); + log.info("Session:{}", findSession); + LoginUser loginUser = new LoginUser(findSession); + + User findUser = userSearchUseCase.findById(loginUser.getUserId()); + ModelMap modelMap = createModelMap(findUser); + response.setStatus(OK); + return new ModelAndView("/my-info.html", modelMap); + } + + private ModelMap createModelMap(User findUser) { + ModelMap modelMap = new ModelMap(); + modelMap.put("username", findUser.getUsername()); + modelMap.put("password", findUser.getPassword()); + return modelMap; + } +} diff --git a/app/src/main/java/project/server/app/core/web/user/presentation/validator/UserValidator.java b/app/src/main/java/project/server/app/core/web/user/presentation/validator/UserValidator.java index 6d0f626..427c7c7 100644 --- a/app/src/main/java/project/server/app/core/web/user/presentation/validator/UserValidator.java +++ b/app/src/main/java/project/server/app/core/web/user/presentation/validator/UserValidator.java @@ -1,12 +1,13 @@ package project.server.app.core.web.user.presentation.validator; import project.server.app.common.exception.InvalidParameterException; +import project.server.app.common.exception.UnAuthorizedException; import project.server.mvc.springframework.annotation.Component; @Component public class UserValidator { - public void validateSignUp( + public void validateSignUpInfo( String username, String password ) { @@ -17,4 +18,22 @@ public void validateSignUp( throw new InvalidParameterException(username, password); } } + + public void validateLoginInfo( + String username, + String password + ) { + if (username == null || password == null) { + throw new InvalidParameterException(username, password); + } + if (username.isBlank() || password.isBlank()) { + throw new InvalidParameterException(username, password); + } + } + + public void validateSessionId(Long userId) { + if (userId == null) { + throw new UnAuthorizedException(); + } + } } diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/resources/assets/400.jpeg b/app/src/main/resources/assets/400.jpeg new file mode 100644 index 0000000..ce452cf Binary files /dev/null and b/app/src/main/resources/assets/400.jpeg differ diff --git a/app/src/main/resources/assets/favicon.png b/app/src/main/resources/assets/favicon.png new file mode 100644 index 0000000..c22fa19 Binary files /dev/null and b/app/src/main/resources/assets/favicon.png differ diff --git a/app/src/main/resources/assets/home-background.jpg b/app/src/main/resources/assets/home-background.jpg new file mode 100644 index 0000000..0ca6e2c Binary files /dev/null and b/app/src/main/resources/assets/home-background.jpg differ diff --git a/app/src/main/resources/assets/my-info-background.jpg b/app/src/main/resources/assets/my-info-background.jpg new file mode 100644 index 0000000..aa4e5a1 Binary files /dev/null and b/app/src/main/resources/assets/my-info-background.jpg differ diff --git a/app/src/main/resources/assets/winter.jpg b/app/src/main/resources/assets/winter.jpg new file mode 100644 index 0000000..bb246c9 Binary files /dev/null and b/app/src/main/resources/assets/winter.jpg differ diff --git a/app/src/main/resources/main.js b/app/src/main/resources/main.js new file mode 100644 index 0000000..6a2cdf1 --- /dev/null +++ b/app/src/main/resources/main.js @@ -0,0 +1,154 @@ +"use strict"; + +const navbar = document.querySelector("#navbar"); +const navbarHeight = navbar.getBoundingClientRect().height; + +document.addEventListener("scroll", () => { + if (window.scrollY > navbarHeight) { + navbar.classList.add("navbar--dark"); + } else { + navbar.classList.remove("navbar--dark"); + } +}); + +const navbarToggleBtn = document.querySelector(".navbar__toggle-btn"); +const navbarMenu = document.querySelector(".navbar__menu"); + +navbarMenu.addEventListener("click", (event) => { + const target = event.target; + const link = target.dataset.link; + if (link == null) { + return; + } + navbarMenu.classList.remove("open"); + scrollIntoView(link); +}); + +navbarToggleBtn.addEventListener("click", () => { + navbarMenu.classList.toggle("open"); +}); + +const contactBtn = document.querySelector(".home__contact"); +contactBtn.addEventListener("click", () => { + scrollIntoView("#contact"); +}); + +const home = document.querySelector(".home__container"); +const homeHeight = home.getBoundingClientRect().height; + +document.addEventListener("scroll", () => { + if (window.scrollY > homeHeight) { + return; + } + const percentVisible = (homeHeight - window.scrollY) / homeHeight; + home.style.opacity = percentVisible; +}); + +const arrowUp = document.querySelector(".arrow-up"); +document.addEventListener("scroll", () => { + if (window.scrollY > homeHeight) { + arrowUp.classList.add("visible"); + } else { + arrowUp.classList.remove("visible"); + } +}); + +arrowUp.addEventListener("click", () => { + scrollIntoView("#home"); +}); + +const workBtnContainer = document.querySelector(".work__categories"); +const projectContainer = document.querySelector(".work__projects"); +const projects = document.querySelectorAll(".project"); +workBtnContainer.addEventListener("click", (e) => { + const element = e.target; + const filter = + getDatasetFilter(element) || getDatasetFilter(element.parentNode); + if (filter == null) { + return; + } + + const selected = document.querySelector(".category__btn.selected"); + selected.classList.remove("selected"); + const target = + e.target.nodeName === "BUTTON" ? e.target : e.target.parentNode; + target.classList.add("selected"); + + projectContainer.classList.add("anim-out"); + setTimeout(() => { + projects.forEach((project) => { + if (filter === "*" || filter === project.dataset.type) { + project.classList.remove("invisible"); + } else { + project.classList.add("invisible"); + } + }); + projectContainer.classList.remove("anim-out"); + }, 300); +}); + +function getDatasetFilter(e) { + return e.dataset.filter; +} + +function scrollIntoView(selector) { + const scrollTo = document.querySelector(selector); + scrollTo.scrollIntoView({ behavior: "smooth" }); + selectNavItem(navItems[sectionIds.indexOf(selector)]); +} + +const sectionIds = [ + "#home", + "#about", + "#skills", + "#work", + "#testimonials", + "#contact", +]; + +const sections = sectionIds.map((id) => document.querySelector(id)); +const navItems = sectionIds.map((id) => + document.querySelector(`[data-link="${id}"]`) +); + +const observerOptions = { + root: null, + rootMargin: "0px", + threshold: "0.3", +}; + +function selectNavItem(selected) { + selectedNavItem.classList.remove("active"); + selectedNavItem = selected; + selectedNavItem.classList.add("active"); +} + +let selectedNavIndex = 0; +let selectedNavItem = navItems[0]; +const observerCallback = (entries, observer) => { + entries.forEach((entry) => { + if (!entry.isIntersecting && entry.intersectionRatio > 0) { + const index = sectionIds.indexOf(`#${entry.target.id}`); + if (entry.boundingClientRect.y < 0) { + selectedNavIndex = index + 1; + } else { + selectedNavIndex = index - 1; + } + } + }); +}; + +const observer = new IntersectionObserver(observerCallback, observerOptions); +sections.forEach((section) => observer.observe(section)); + +window.addEventListener("wheel", () => { + if (window.scrollY === 0) { + selectedNavIndex = 0; + } else if ( + Math.round(window.scrollY + window.innerHeight) >= + document.body.clientHeight + ) { + selectedNavIndex = navItems.length - 1; + } + selectNavItem(navItems[selectedNavIndex]); +}); diff --git a/app/src/main/resources/sign-in.css b/app/src/main/resources/sign-in.css new file mode 100644 index 0000000..fc53324 --- /dev/null +++ b/app/src/main/resources/sign-in.css @@ -0,0 +1,217 @@ +@import url("https://fonts.googleapis.com/css?family=Proxima Nova:400,800"); + +* { + box-sizing: border-box; +} + +.sign-body { + background: #44c3e7; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + font-family: "Montserrat", sans-serif; + height: 100vh; + margin: -20px 0 50px; +} + +h1 { + font-weight: bold; + margin: 0; +} + +h2 { + text-align: center; +} + +p { + font-size: 16px; + font-weight: 100; + line-height: 20px; + letter-spacing: 0.5px; + margin: 20px 0 30px; +} + +span { + font-size: 12px; +} + +a { + color: #333; + font-size: 14px; + text-decoration: none; + margin: 15px 0; +} + +button { + border-radius: 20px; + border: 1px solid #44c3e7; + background-color: #44c3e7; + color: #ffffff; + font-size: 12px; + font-weight: bold; + padding: 12px 45px; + letter-spacing: 1px; + text-transform: uppercase; + transition: transform 80ms ease-in; +} + +button:active { + transform: scale(0.95); +} + +button:focus { + outline: none; +} + +button.ghost { + background-color: transparent; + border-color: #ffffff; +} + +form { + background-color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + padding: 0 50px; + height: 100%; + text-align: center; +} + +input { + background-color: #eee; + border: none; + padding: 12px 15px; + margin: 8px 0; + width: 100%; +} + +.container { + background-color: #fff; + border-radius: 10px; + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); + position: relative; + overflow: hidden; + width: 768px; + max-width: 100%; + min-height: 480px; +} + +.form-container { + position: absolute; + top: 0; + height: 100%; + transition: all 0.6s ease-in-out; +} + +.sign-in-container { + left: 0; + width: 50%; + z-index: 2; +} + +.container.right-panel-active .sign-in-container { + transform: translateX(100%); +} + +.sign-up-container { + left: 0; + width: 50%; + opacity: 0; + z-index: 1; +} + +.container.right-panel-active .sign-up-container { + transform: translateX(100%); + opacity: 1; + z-index: 5; + animation: show 0.6s; +} + +@keyframes show { + 0%, + 49.99% { + opacity: 0; + z-index: 1; + } + + 50%, + 100% { + opacity: 1; + z-index: 5; + } +} + +.overlay-container { + position: absolute; + top: 0; + left: 50%; + width: 50%; + height: 100%; + overflow: hidden; + transition: transform 0.6s ease-in-out; + z-index: 100; +} + +.container.right-panel-active .overlay-container { + transform: translateX(-100%); +} + +.overlay { + background: #3790cc; + background: -webkit-linear-gradient(to right, #44c3e7, #3790cc); + background: linear-gradient(to right, #44c3e7, #3790cc); + background-repeat: no-repeat; + background-size: cover; + background-position: 0 0; + color: #ffffff; + position: relative; + left: -100%; + height: 100%; + width: 200%; + transform: translateX(0); + transition: transform 0.6s ease-in-out; +} + +.container.right-panel-active .overlay { + transform: translateX(50%); +} + +.overlay-panel { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + padding: 0 40px; + text-align: center; + top: 0; + height: 100%; + width: 50%; + transform: translateX(0); + transition: transform 0.6s ease-in-out; +} + +.overlay-left { + transform: translateX(-20%); +} + +.container.right-panel-active .overlay-left { + transform: translateX(0); +} + +.overlay-right { + right: 0; + transform: translateX(0); +} + +.container.right-panel-active .overlay-right { + transform: translateX(20%); +} + +.sign__title { + margin: 32px 0; + color: var(--color-white); +} \ No newline at end of file diff --git a/app/src/main/resources/static/400.html b/app/src/main/resources/static/400.html new file mode 100644 index 0000000..5968cb2 --- /dev/null +++ b/app/src/main/resources/static/400.html @@ -0,0 +1,27 @@ + + + + + + + 400 Error Page + + + +400 Error + + diff --git a/app/src/main/resources/static/index.html b/app/src/main/resources/static/index.html new file mode 100644 index 0000000..234d0c8 --- /dev/null +++ b/app/src/main/resources/static/index.html @@ -0,0 +1,176 @@ + + + + + + + Spring Mission + + + + + + + + + + + + + + + + +
+
+

+ Jun +

+ +
+
+ +
+

About me

+

+ 12๋ฒˆ์˜ ๊ณต๋ชจ์ „์— ์ฐธ์—ฌํ•˜๋ฉฐ, ๋ฌธ์ œํ•ด๊ฒฐ์˜ ์žฌ๋ฏธ๋ฅผ ๋А๊ปด ๊ฐœ๋ฐœ์„ ์‹œ์ž‘ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ์ฝ”๋“œ๋กœ ๋” ๋งŽ์€ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์„ฑ์žฅํ•˜๋ฉฐ,
+ ๋ฐ˜๋ณต์ ์ธ ์—…๋ฌด๋ฅผ ์ž๋™ํ™”์‹œํ‚ค๋Š” ๊ฒƒ์„ ์ข‹์•„ํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„์™€ ์ธํ”„๋ผ์— ๊ด€์‹ฌ์ด ๋งŽ์œผ๋ฉฐ, ๊ฐœ์ธ ์„œ๋ฒ„์—์„œ ์Šค์ผ€์ค„/์ผ์ •์„ ์ž๋™ํ™”ํ•ด ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. +

+
+
+
+ +
+

Database

+
MySQL 8.0
+
+
+
+ +
+

Backend

+
+ Java 17, SpringBoot
JPA, QueryDSL +
+
+
+
+ +
+

Infra

+
AWS, Docker, Nginx
+
+
+
+ + + + + + + diff --git a/app/src/main/resources/static/my-info.html b/app/src/main/resources/static/my-info.html new file mode 100644 index 0000000..401fe79 --- /dev/null +++ b/app/src/main/resources/static/my-info.html @@ -0,0 +1,117 @@ + + + + + + + + ํšŒ์› ์ƒ์„ธ์ •๋ณด - Bootstrap + + + + + + +
+
+
+

ํšŒ์› ์ƒ์„ธ์ •๋ณด

+
+
+
+ + +
+ ID๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. +
+
+
+ + +
+ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. +
+
+
+
+ + +
+ ์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. +
+
+
+ + +
+ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. +
+
+
+ + +
+
+
+ + +
+
+ +
+
+
+ + +
+
+
+ + + + diff --git a/app/src/main/resources/static/sign-in.html b/app/src/main/resources/static/sign-in.html new file mode 100644 index 0000000..dfd1138 --- /dev/null +++ b/app/src/main/resources/static/sign-in.html @@ -0,0 +1,145 @@ + + + + + + + + + Origogi's Portfolio + + + + + + + + + + + + + + + + +
+

Member Access

+
+ + + + + +
+
+
+

Welcome Back!

+

To keep connected with us please login with your personal info

+ +
+ +
+

If you're first-com

+

Enter your personal details and start journey with us

+ +
+
+
+ +
+
+ + + + diff --git a/app/src/main/resources/style.css b/app/src/main/resources/style.css new file mode 100644 index 0000000..3f8545c --- /dev/null +++ b/app/src/main/resources/style.css @@ -0,0 +1,490 @@ +:root { + --color-white: #ffffff; + --color-light-white: #eeeeee; + --color-dark-white: #bdbdbd; + --color-pink: #fe918d; + --color-dark-pink: #ff6863; + --color-dark-grey: #4d4d4d; + --color-grey: #616161; + --color-light-grey: #7c7979; + --color-blue: #eeeeee; + --color-dark-blue: #3790cc; + + --color-yellow: #fff7d1; + --color-orange: #feb546; + --color-black: #000000; + + /* Font size */ + --font-large: 48px; + --font-medium: 28px; + --font-regular: 18px; + --font-small: 16px; + --font-micro: 14px; + + /* Font weight */ + --weight-bold: 700; + --weight-semi-bold: 600; + --weight-regular: 400; + + /* Size */ + --size--border-radius: 4px; + + /* Animation */ + --animation-duration: 300ms; +} + +body { + font-family: "Open Sans", sans-serif; + margin: 0; + cursor: default; +} + +a { + color: var(--color-white); + text-decoration: none; +} + +ul { + padding-left: 0; +} + +li { + list-style: none; +} + +button { + background-color: transparent; + cursor: pointer; + border: none; + outline: none; +} + +/* Universal tag */ +* { + box-sizing: border-box; +} + +/* Typography */ + +h1 { + font-size: var(--font-large); + font-weight: var(--weight-bold); + color: var(--color-black); + margin: 16px 0px; +} + +h2 { + font-size: var(--font-medium); + font-weight: var(--weight-semi-bold); + color: var(--color-black); + margin: 8px 0px; +} + +h3 { + font-size: var(--font-regular); + font-weight: var(--weight-regular); + color: var(--color-black); + margin: 8px 0px; +} + +P { + font-size: var(--font-regular); + font-weight: var(--weight-regular); + color: var(--color-black); + margin: 4px 0px; +} + +#navbar { + width: 100%; + position: fixed; + display: flex; + justify-content: space-between; + background-color: transparent; + align-items: center; + color: var(--color-white); + padding: 16px; + z-index: 1; + transition: all var(--animation-duration) ease-in-out; +} + +#navbar.navbar--dark { + padding: 8px; + background-color: var(--color-blue); +} + +.navbar__logo { + display: flex; + align-items: center; + height: 40px; + font-size: var(--font-medium); + font-weight: var(--weight-semi-bold); +} + +.navbar__logo > img { + height: 100%; + width: 100%; + object-fit: contain; + margin-right: 10px; +} + +.navbar__menu { + display: flex; +} + +.navbar__menu__item { + padding: 8px 12px; + margin: 0 4px; + cursor: pointer; + border: 1px solid transparent; + border-radius: var(--size--border-radius); +} + +.navbar__menu__item.active { + border: 1px solid var(--color-white); +} + +.navbar__toggle-btn { + position: absolute; + top: 24px; + right: 32px; + font-size: 24px; + color: var(--color-white); + display: none; +} + +.navbar__menu__item:hover { + border: 1px solid white; + border-radius: var(--size--border-radius); + background-color: var(--color-dark-blue); +} + +#home { + background: url("/assets/home-background.jpg") center/cover no-repeat; + padding: 40px; + padding-top: 100px; + text-align: center; +} + +.home__avatar { + width: 250px; + height: 250px; + border-radius: 50%; + border: 5px solid var(--color-light-white); +} + +.home__title, +.home__description { + color: var(--color-white); +} + +.home__contact { + color: var(--color-white); + font-size: var(--font-regular); + font-weight: var(--weight-bold); + margin: 24px; + padding: 8px 12px; + border: 2px solid var(--color-white); + border-radius: var(--size--border-radius); +} + +.home__contact:hover { + background-color: var(--color-orange); +} + +.section { + padding: 3px; + text-align: center; + margin: auto; +} + +.section__container { + max-width: 1200px; + margin: auto; +} + +.about__majors { + display: flex; + justify-content: space-between; + margin: 80px 0; +} + +.major__icon { + width: 170px; + height: 170px; + font-size: 70px; + line-height: 170px; + margin: auto; + border: 1px solid var(--color-blue); + color: var(--color-blue); + border-radius: 50%; + margin-bottom: 16px; +} + +.major__icon:hover i { + color: var(--color-pink); + transform: rotate(-30deg) scale(1.1); +} + +.major__icon > i { + transition: all var(--animation-duration) ease; +} + +.major__title, +.major__description { + color: var(--color-dark-grey); +} + +.major__description { + font-size: var(--font-small); +} + +.job { + display: flex; + align-items: center; + margin-bottom: 16px; +} + +.job img { + height: 50px; + width: 50px; + object-fit: contain; + margin-right: 30px; +} + +.job__description { + margin: 0 16px; + text-align: left; +} + +.job__name, +.job__period { + color: var(--color-light-grey); +} + +.job__name { + font-size: var(--font-small); +} + +.job__period { + font-size: var(--font-micro); +} + +#skills { + background-color: var(--color-yellow); +} + +.skill { + margin-bottom: 32px; +} + +.skill__value { + height: 3px; + background-color: var(--color-orange); +} + +.skillset { + display: flex; + background-color: var(--color-light-grey); + color: var(--color-light-white); + margin: 20px 0; +} + +.skillset__left { + flex-basis: 60%; + background-color: var(--color-dark-grey); + padding: 20px 40px; +} + +.skillset__title { + color: var(--color-white); +} + +.skill__bar { + width: 100%; + height: 3px; + background-color: var(--color-grey); +} + +.skill__description { + display: flex; + justify-content: space-between; +} + +.skillset__right { + flex-basis: 40%; +} + +.tools { + background-color: var(--color-grey); +} + +.tools, +.etc { + padding: 20px; +} + +.work__categories { + margin: 40px; +} + +.category__btn { + border: 1px solid var(--color-dark-white); + border-radius: var(--size--border-radius); + font-size: var(--font-regular); + padding: 8px 48px; + position: relative; +} + +.category__btn.selected, +.category__btn:hover { + background-color: var(--color-pink); + color: var(--color-white); +} + +.category__btn.selected .category__count, +.category__btn:hover .category__count { + top: 0; + opacity: 1; +} + +.category__count { + display: inline-block; + position: absolute; + line-height: 24px; + background-color: var(--color-orange); + border-radius: 50%; + color: var(--color-white); + width: 24px; + height: 24px; + right: 16px; + top: -20px; + + opacity: 0; + transition: all var(--animation-duration) ease-in; +} + +#contact { + background-color: var(--color-blue); +} + +.contact__title, +.contact__rights, +.contact__email { + color: var(--color-white); +} + +.contact__title { + margin: 32px 0; +} + +.contact__links { + font-size: var(--font-large); + margin: 12px 0; + transition: all var(--animation-duration) ease-out; +} + +.contact__links i:hover { + transform: scale(1.1); + color: var(--color-yellow); +} + +/* Scroll */ +.arrow-up { + position: fixed; + width: 70px; + height: 70px; + bottom: 50px; + right: 50px; + font-size: 50px; + color: var(--color-white); + background-color: var(--color-blue); + border-radius: 50%; + opacity: 0; + pointer-events: none; + transition: opacity var(--animation-duration) ease-in; +} + +.arrow-up.visible { + opacity: 1; + pointer-events: auto; +} + +/* For below 768px screen width */ +@media screen and (max-width: 768px) { + :root { + /* Font size */ + --font-large: 30px; + --font-medium: 18px; + --font-regular: 16px; + --font-small: 14px; + --font-micro: 12px; + } + + .navbar__toggle-btn { + display: block; + font-size: var(--font-medium); + top: 26px; + right: 16px; + } + + #navbar { + flex-direction: column; + align-items: flex-start; + background-color: var(--color-blue); + } + + #navbar.navbar--dark { + padding: 16px; + } + + .navbar__menu { + flex-direction: column; + text-align: center; + width: 100%; + display: none; + } + + .navbar__menu.open { + display: block; + } + + .section { + padding: 16px; + padding-top: 40px; + } + + .about__majors { + flex-direction: column; + margin-top: 30px; + margin-bottom: 0px; + } + + .major { + margin-bottom: 30px; + } + + .skillset { + flex-direction: column; + } + + .category__btn { + width: 100%; + margin: 4px 0px; + } + + .arrow-up { + width: 50px; + height: 50px; + font-size: 30px; + line-height: 50px; + right: 16px; + bottom: 16px; + } + + .navbar__logo > img { + height: 30px; + width: 30px; + object-fit: contain; + margin-right: 16px; + } +} diff --git a/app/src/test/java/project/server/app/common/fixture/user/LoginUserFixture.java b/app/src/test/java/project/server/app/common/fixture/user/LoginUserFixture.java new file mode 100644 index 0000000..505b829 --- /dev/null +++ b/app/src/test/java/project/server/app/common/fixture/user/LoginUserFixture.java @@ -0,0 +1,22 @@ +package project.server.app.common.fixture.user; + +import java.time.LocalDateTime; +import java.util.UUID; +import project.server.app.common.login.LoginUser; +import project.server.app.common.login.Session; + +public final class LoginUserFixture { + + private LoginUserFixture() { + throw new AssertionError("์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ์‹์œผ๋กœ ์ƒ์„ฑ์ž๋ฅผ ํ˜ธ์ถœํ•ด์ฃผ์„ธ์š”."); + } + + public static LoginUser createLoginUser( + Long userId, + UUID sessionId, + LocalDateTime expiredAt + ) { + Session session = new Session(userId, sessionId.toString(), expiredAt); + return new LoginUser(session); + } +} diff --git a/app/src/test/java/project/server/app/common/fixture/user/UserFixture.java b/app/src/test/java/project/server/app/common/fixture/user/UserFixture.java index 4065c0b..1bd8b46 100644 --- a/app/src/test/java/project/server/app/common/fixture/user/UserFixture.java +++ b/app/src/test/java/project/server/app/common/fixture/user/UserFixture.java @@ -10,7 +10,15 @@ private UserFixture() { public static User createUser() { return new User( - 1L, + null, + "Steve-Jobs", + "HelloWorld145" + ); + } + + public static User createUser(Long userId) { + return new User( + userId, "Steve-Jobs", "HelloWorld145" ); diff --git a/app/src/test/java/project/server/app/test/integrationtest/user/UserDeleteIntegrationTest.java b/app/src/test/java/project/server/app/test/integrationtest/user/UserDeleteIntegrationTest.java new file mode 100644 index 0000000..cf582aa --- /dev/null +++ b/app/src/test/java/project/server/app/test/integrationtest/user/UserDeleteIntegrationTest.java @@ -0,0 +1,37 @@ +package project.server.app.test.integrationtest.user; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import project.server.app.common.exception.BusinessException; +import static project.server.app.common.fixture.user.UserFixture.createUser; +import project.server.app.common.login.LoginUser; +import project.server.app.core.domain.user.User; +import project.server.app.core.web.user.application.UserDeleteUseCase; +import project.server.app.core.web.user.application.UserSaveUseCase; +import project.server.app.core.web.user.application.UserSearchUseCase; +import project.server.app.core.web.user.application.service.UserService; +import project.server.app.core.web.user.exception.UserNotFoundException; +import project.server.app.test.integrationtest.IntegrationTestBase; +import static project.server.mvc.springframework.context.ApplicationContext.getBean; + +@DisplayName("[IntegrationTest] ์‚ฌ์šฉ์ž ์‚ญ์ œ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") +class UserDeleteIntegrationTest extends IntegrationTestBase { + + private final UserSaveUseCase userSaveUseCase = getBean(UserService.class); + private final UserSearchUseCase userSearchUseCase = getBean(UserService.class); + private final UserDeleteUseCase userDeleteUseCase = getBean(UserService.class); + + @Test + @DisplayName("์‚ญ์ œ๋œ ์‚ฌ์šฉ์ž๋ฅผ ์‚ญ์ œํ•˜๋ คํ•˜๋ฉด UserNotFoundException์ด ๋ฐœ์ƒํ•œ๋‹ค.") + void test() { + User savedUser = userSaveUseCase.save(createUser()); + LoginUser loginUser = new LoginUser(savedUser.getId(), null); + userDeleteUseCase.delete(loginUser); + + assertThatThrownBy(() -> userDeleteUseCase.delete(loginUser)) + .isInstanceOf(BusinessException.class) + .isExactlyInstanceOf(UserNotFoundException.class) + .hasMessage("์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } +} diff --git a/app/src/test/java/project/server/app/test/integrationtest/user/UserLoginIntegrationTest.java b/app/src/test/java/project/server/app/test/integrationtest/user/UserLoginIntegrationTest.java new file mode 100644 index 0000000..f94a117 --- /dev/null +++ b/app/src/test/java/project/server/app/test/integrationtest/user/UserLoginIntegrationTest.java @@ -0,0 +1,52 @@ +package project.server.app.test.integrationtest.user; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import project.server.app.common.exception.UnAuthorizedException; +import project.server.app.common.login.Session; +import project.server.app.core.domain.user.User; +import project.server.app.core.web.user.application.UserLoginUseCase; +import project.server.app.core.web.user.application.UserSaveUseCase; +import project.server.app.core.web.user.application.service.UserLoginService; +import project.server.app.core.web.user.application.service.UserService; +import project.server.app.test.integrationtest.IntegrationTestBase; +import static project.server.mvc.springframework.context.ApplicationContext.getBean; + +@DisplayName("[IntegrationTest] ๋กœ๊ทธ์ธ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") +class UserLoginIntegrationTest extends IntegrationTestBase { + + private final UserSaveUseCase userSaveUseCase = getBean(UserService.class); + private final UserLoginUseCase loginUseCase = getBean(UserLoginService.class); + + @Test + @DisplayName("์ •์ƒ์ ์œผ๋กœ ๋กœ๊ทธ์ธ์ด ๋˜๋ฉด ์„ธ์…˜์ด ๋ฐœ๊ธ‰๋œ๋‹ค.") + void sessionCreateTest() { + User savedUser = userSaveUseCase.save(new User("Steve-Jobs", "Helloworld")); + Session session = loginUseCase.login(savedUser.getUsername(), savedUser.getPassword()); + + assertNotNull(session); + } + + @Test + @DisplayName("์„ธ์…˜์ด ์กด์žฌํ•˜๋ฉด ์ด๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค.") + void sessionSearchTest() { + User newUser = new User("Steve-Jobs", "Helloworld"); + User savedUser = userSaveUseCase.save(newUser); + Session session = loginUseCase.login(savedUser.getUsername(), savedUser.getPassword()); + + assertNotNull(loginUseCase.findSessionById(session.userId())); + } + + @Test + @DisplayName("์„ธ์…˜์ด ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด UnAuthorizedException์ด ๋ฐœ์ƒํ•œ๋‹ค.") + void sessionSearchFailureTest() { + Long invalidSessionId = Long.MAX_VALUE; + + assertThatThrownBy(() -> loginUseCase.findSessionById(invalidSessionId)) + .isInstanceOf(RuntimeException.class) + .isExactlyInstanceOf(UnAuthorizedException.class) + .hasMessage("๊ถŒํ•œ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } +} diff --git a/app/src/test/java/project/server/app/test/integrationtest/user/UserSaveIntegrationTest.java b/app/src/test/java/project/server/app/test/integrationtest/user/UserSaveIntegrationTest.java index ad2a132..1e53a64 100644 --- a/app/src/test/java/project/server/app/test/integrationtest/user/UserSaveIntegrationTest.java +++ b/app/src/test/java/project/server/app/test/integrationtest/user/UserSaveIntegrationTest.java @@ -72,7 +72,7 @@ void userSaveSynchronizedTest() throws InterruptedException { Set userIds = ConcurrentHashMap.newKeySet(); for (int index = 1; index <= fixedUserCount; index++) { - User newUser = new User("User" + index, "Password" + index); + User newUser = new User("Username" + index, "Password" + index); executorService.submit(() -> { try { User savedUser = userSaveUseCase.save(newUser); diff --git a/app/src/test/java/project/server/app/test/unittest/login/SessionUnitTest.java b/app/src/test/java/project/server/app/test/unittest/login/SessionUnitTest.java new file mode 100644 index 0000000..f656bac --- /dev/null +++ b/app/src/test/java/project/server/app/test/unittest/login/SessionUnitTest.java @@ -0,0 +1,29 @@ +package project.server.app.test.unittest.login; + +import java.time.LocalDateTime; +import java.util.UUID; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import project.server.app.common.login.Session; + +@DisplayName("[UnitTest] ์„ธ์…˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") +class SessionUnitTest { + + @Test + @DisplayName("์„ธ์…˜์€ sessionId๋กœ ๊ฐ์ฒด๋ฅผ ๋น„๊ตํ•œ๋‹ค.") + void test() { + Long userId = 1L; + LocalDateTime now = LocalDateTime.now(); + UUID uuid = UUID.randomUUID(); + Session session = new Session(userId, uuid.toString(), now); + Session differentSession = new Session(userId, UUID.randomUUID().toString(), now); + + assertAll( + () -> assertTrue(session.equals(session)), + () -> assertFalse(session.equals(differentSession)) + ); + } +} diff --git a/app/src/test/java/project/server/app/test/unittest/user/LoginUserUnitTest.java b/app/src/test/java/project/server/app/test/unittest/user/LoginUserUnitTest.java new file mode 100644 index 0000000..b455d29 --- /dev/null +++ b/app/src/test/java/project/server/app/test/unittest/user/LoginUserUnitTest.java @@ -0,0 +1,70 @@ +package project.server.app.test.unittest.user; + +import java.time.LocalDateTime; +import java.util.UUID; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import static project.server.app.common.fixture.user.LoginUserFixture.createLoginUser; +import project.server.app.common.login.LoginUser; +import project.server.app.common.login.Session; + +@DisplayName("[UnitTest] ๋กœ๊ทธ์ธ ์œ ์ € ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") +class LoginUserUnitTest { + + @Test + @DisplayName("์˜ฌ๋ฐ”๋ฅธ ์ •๋ณด๊ฐ€ ์ž…๋ ฅ๋˜๋ฉด ๋กœ๊ทธ์ธ ๋œ ์‚ฌ์šฉ์ž ๊ฐ์ฒด๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค.") + void loginUserCreateTest() { + Long userId = 1L; + LocalDateTime now = LocalDateTime.now(); + UUID uuid = UUID.randomUUID(); + Session session = new Session(userId, uuid.toString(), now); + + assertAll( + () -> assertNotNull(createLoginUser(userId, uuid, now)), + () -> new LoginUser(session) + ); + } + + @Test + @DisplayName("๋กœ๊ทธ์ธ ์œ ์ €๋Š” ์‚ฌ์šฉ์ž PK ๊ฐ’์œผ๋กœ ๊ฐ์ฒด๋ฅผ ๋น„๊ตํ•œ๋‹ค.") + void loginUserEqualsTest() { + LocalDateTime now = LocalDateTime.now(); + UUID uuid = UUID.randomUUID(); + + assertAll( + () -> assertEquals(createLoginUser(1L, uuid, now), createLoginUser(1L, uuid, now)), + () -> assertNotEquals(createLoginUser(2L, uuid, now), createLoginUser(1L, uuid, now)) + ); + } + + @Test + @DisplayName("์„ธ์…˜์ด ์˜ฌ๋ฐ”๋ฅด๋‹ค๋ฉด ์˜ฌ๋ฐ”๋ฅธ ์‚ฌ์šฉ์ž๋‹ค.") + void loginUserValidTest() { + Long userId = 1L; + LocalDateTime now = LocalDateTime.now(); + UUID uuid = UUID.randomUUID(); + Session session = new Session(userId, uuid.toString(), now.plusMinutes(15)); + + LoginUser loginUser = new LoginUser(session); + + assertTrue(loginUser.isValid()); + } + + @Test + @DisplayName("์„ธ์…˜์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š๋‹ค๋ฉด ์˜ฌ๋ฐ”๋ฅธ ์‚ฌ์šฉ์ž๊ฐ€ ์•„๋‹ˆ๋‹ค.") + void loginUserInValidTest() { + Long userId = 1L; + LocalDateTime now = LocalDateTime.now().minusDays(3); + UUID uuid = UUID.randomUUID(); + Session session = new Session(userId, uuid.toString(), now); + LoginUser loginUser = new LoginUser(session); + + assertFalse(loginUser.isValid()); + } +} diff --git a/app/src/test/java/project/server/app/test/unittest/user/UserUnitTest.java b/app/src/test/java/project/server/app/test/unittest/user/UserUnitTest.java index 511af33..5952b23 100644 --- a/app/src/test/java/project/server/app/test/unittest/user/UserUnitTest.java +++ b/app/src/test/java/project/server/app/test/unittest/user/UserUnitTest.java @@ -21,8 +21,8 @@ void userCreateTest() { @Test @DisplayName("equals๋ฅผ ์žฌ์ •์˜ ํ–ˆ์„ ๋•Œ, ๊ฐ’์ด ๊ฐ™๋‹ค๋ฉด ๊ฐ™์€ ๊ฐ์ฒด๋กœ ์ธ์‹ํ•œ๋‹ค.") void userEqualsTest() { - User user = createUser(); - User sameUser = createUser(); + User user = new User(1L, "John-Wak", "Hello0145"); + User sameUser = new User(1L, "John-Wak", "Hello0145"); assertEquals(user, sameUser); } @@ -30,7 +30,7 @@ void userEqualsTest() { @Test @DisplayName("equals๋ฅผ ์žฌ์ •์˜ ํ–ˆ์„ ๋•Œ, ๊ฐ’์ด ๋‹ค๋ฅด๋‹ค๋ฉด ๋‹ค๋ฅธ ๊ฐ์ฒด๋กœ ์ธ์‹ํ•œ๋‹ค.") void differentUserEqualsTest() { - User user = createUser(); + User user = new User(2L, "John-Wak", "Hello0145"); User differentUser = new User(3L, "John-Wak", "Hello0145"); assertNotEquals(user, differentUser); diff --git a/app/src/test/java/project/server/app/test/unittest/user/UserValidatorUnitTest.java b/app/src/test/java/project/server/app/test/unittest/user/UserValidatorUnitTest.java index 7c92da2..e9e0f32 100644 --- a/app/src/test/java/project/server/app/test/unittest/user/UserValidatorUnitTest.java +++ b/app/src/test/java/project/server/app/test/unittest/user/UserValidatorUnitTest.java @@ -1,11 +1,14 @@ package project.server.app.test.unittest.user; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullAndEmptySource; import project.server.app.common.exception.InvalidParameterException; +import project.server.app.common.exception.UnAuthorizedException; import project.server.app.core.web.user.presentation.validator.UserValidator; @DisplayName("[UnitTest] ์‚ฌ์šฉ์ž ๊ฒ€์ฆ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") @@ -22,7 +25,7 @@ void setUp() { @NullAndEmptySource @DisplayName("์‚ฌ์šฉ์ž ์ด๋ฆ„์ด Null ๋˜๋Š” ๋นˆ ๊ฐ’์ด๋ฉด InvalidParameterException์ด ๋ฐœ์ƒํ•œ๋‹ค.") void usernameNullOrBlank(String parameter) { - assertThatThrownBy(() -> validator.validateSignUp(parameter, "helloworld")) + assertThatThrownBy(() -> validator.validateSignUpInfo(parameter, "helloworld")) .isInstanceOf(RuntimeException.class) .isExactlyInstanceOf(InvalidParameterException.class); } @@ -31,8 +34,22 @@ void usernameNullOrBlank(String parameter) { @NullAndEmptySource @DisplayName("์‚ฌ์šฉ์ž ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ Null ๋˜๋Š” ๋นˆ ๊ฐ’์ด๋ฉด InvalidParameterException์ด ๋ฐœ์ƒํ•œ๋‹ค.") void passwordNullOrBlank(String parameter) { - assertThatThrownBy(() -> validator.validateSignUp("Steve-Jobs", parameter)) + assertThatThrownBy(() -> validator.validateSignUpInfo("Steve-Jobs", parameter)) .isInstanceOf(RuntimeException.class) .isExactlyInstanceOf(InvalidParameterException.class); } + + @Test + @DisplayName("์‚ฌ์šฉ์ž ์•„์ด๋””๊ฐ€ null์ด๋ฉด UnAuthorizedException์ด ๋ฐœ์ƒํ•œ๋‹ค.") + void sessionIdNullTest() { + assertThatThrownBy(() -> validator.validateSessionId(null)) + .isInstanceOf(RuntimeException.class) + .isExactlyInstanceOf(UnAuthorizedException.class); + } + + @Test + @DisplayName("์‚ฌ์šฉ์ž ์•„์ด๋””๊ฐ€ null์ด ์•„๋‹ˆ๋ผ๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.") + void sessionIdValidateTest() { + assertDoesNotThrow(() -> validator.validateSessionId(1L)); + } } diff --git a/app/src/test/java/project/server/app/test/utils/HeaderUtilsUnitTest.java b/app/src/test/java/project/server/app/test/utils/HeaderUtilsUnitTest.java new file mode 100644 index 0000000..3c3d496 --- /dev/null +++ b/app/src/test/java/project/server/app/test/utils/HeaderUtilsUnitTest.java @@ -0,0 +1,34 @@ +package project.server.app.test.utils; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import static project.server.app.common.utils.HeaderUtils.getSessionId; +import project.server.mvc.servlet.http.Cookie; +import project.server.mvc.servlet.http.Cookies; + +@DisplayName("[UnitTest] ํ—ค๋” ์œ ํ‹ธ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") +class HeaderUtilsUnitTest { + + @Test + @DisplayName("์˜ฌ๋ฐ”๋ฅธ ์„ธ์…˜ ์•„์ด๋””๊ฐ€ ์กด์žฌํ•˜๋ฉด, ์ด ๊ฐ’์€ Null์ด ์•„๋‹ˆ๋‹ค.") + void validSessionIdGetTest() { + Cookies cookies = new Cookies(); + cookies.add(new Cookie("sessionId", "1")); + + assertNotNull(getSessionId(cookies)); + } + + @ParameterizedTest + @ValueSource(strings = {"a", "ใ„น", "ใ„น4", "ใ…ฃ", "ใ…", "z", "I"}) + @DisplayName("์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์„ธ์…˜ ์•„์ด๋””๋ผ๋ฉด null์ด ๋ฐ˜ํ™˜๋œ๋‹ค.") + void invalidSessionIdGetTest(String parameter) { + Cookies cookies = new Cookies(); + cookies.add(new Cookie("sessionId", parameter)); + + assertNull(getSessionId(cookies)); + } +} diff --git a/build.gradle b/build.gradle index dfe2b56..7bc580e 100644 --- a/build.gradle +++ b/build.gradle @@ -11,9 +11,9 @@ allprojects { } subprojects { - apply plugin: ("java") - apply plugin: ("checkstyle") - apply plugin: ("pmd") + apply(plugin: "java") + apply(plugin: "checkstyle") + apply(plugin: "pmd") apply(from: "${rootDir}/script/analysis/pmd.gradle") apply(from: "${rootDir}/script/analysis/checkstyle.gradle") diff --git a/gradle.properties b/gradle.properties index a48d39e..c9087fd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,10 @@ ## Project ## group=project.server -## CheckStyle, PMD ## +## CheckStyle, PMD, Jacoco ## checkStyleVersion=10.2 pmdVersion=6.52.0 +jacocoVersion=0.8.8 ## Java ## sourceCompatibility=17 diff --git a/mvc/src/main/java/project/server/mvc/Acceptor.java b/mvc/src/main/java/project/server/mvc/Acceptor.java new file mode 100644 index 0000000..1ce1a67 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/Acceptor.java @@ -0,0 +1,87 @@ +package project.server.mvc; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Set; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import project.server.mvc.tomcat.AsyncRequest; +import project.server.mvc.tomcat.Nio2EndPoint; + +@Slf4j +public class Acceptor implements Runnable { + + private static final int FINISHED = -1; + private static final int BUFFER_CAPACITY = 1024; + + private final Selector selector; + private final Nio2EndPoint nio2EndPoint; + + public Acceptor( + int port, + Nio2EndPoint nio2EndPoint + ) throws Exception { + this.selector = Selector.open(); + this.nio2EndPoint = nio2EndPoint; + initContext(port); + } + + private void initContext(int port) throws IOException { + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + serverSocketChannel.configureBlocking(false); + serverSocketChannel.bind(new InetSocketAddress(port)); + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + } + + @Override + @SneakyThrows + public void run() { + while (true) { + selector.select(); + Set selectedKeys = selector.selectedKeys(); + Iterator keys = selectedKeys.iterator(); + + while (keys.hasNext()) { + SelectionKey key = keys.next(); + + if (key.isAcceptable()) { + acceptSocket(key, selector); + } else if (key.isReadable()) { + read(key); + } + keys.remove(); + } + } + } + + private void acceptSocket( + SelectionKey key, + Selector selector + ) throws IOException { + ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); + SocketChannel socketChannel = serverChannel.accept(); + socketChannel.configureBlocking(false); + socketChannel.register(selector, SelectionKey.OP_READ); + } + + private void read(SelectionKey key) throws Exception { + SocketChannel socketChannel = (SocketChannel) key.channel(); + ByteBuffer buffer = ByteBuffer.allocate(BUFFER_CAPACITY); + int bytesRead = socketChannel.read(buffer); + if (bytesRead == FINISHED) { + socketChannel.close(); + log.info("Connection closed by client."); + return; + } + + buffer.flip(); + new AsyncRequest(socketChannel, buffer) + .run(); + } +} diff --git a/mvc/src/main/java/project/server/mvc/Application.java b/mvc/src/main/java/project/server/mvc/Application.java new file mode 100644 index 0000000..cca29a4 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/Application.java @@ -0,0 +1,33 @@ +package project.server.mvc; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import project.server.mvc.springframework.context.ApplicationContext; +import project.server.mvc.springframework.context.ApplicationContextProvider; + +@Slf4j +public class Application { + + private final Tomcat tomcat; + + @SneakyThrows + public Application( + String packages, + int port, + int threadCount + ) { + initContext(packages); + this.tomcat = new Tomcat(port, threadCount); + } + + private void initContext(String packages) throws Exception { + ApplicationContext context; + context = new ApplicationContext(packages); + ApplicationContextProvider provider = new ApplicationContextProvider(); + provider.setApplicationContext(context); + } + + public void start() { + tomcat.run(); + } +} diff --git a/mvc/src/main/java/project/server/mvc/Tomcat.java b/mvc/src/main/java/project/server/mvc/Tomcat.java new file mode 100644 index 0000000..46f9df0 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/Tomcat.java @@ -0,0 +1,39 @@ +package project.server.mvc; + +import java.util.concurrent.ExecutorService; +import static java.util.concurrent.Executors.newFixedThreadPool; +import project.server.mvc.tomcat.Nio2EndPoint; + +public class Tomcat { + + private static final int DEFAULT_THREAD_COUNT = 200; + private final Acceptor acceptor; + + public Tomcat( + int port, + Integer threadCount + ) throws Exception { + Nio2EndPoint nio2EndPoint = createNio2EndPoint(threadCount); + this.acceptor = new Acceptor(port, nio2EndPoint); + } + + private Nio2EndPoint createNio2EndPoint(Integer threadCount) { + int tomcatThreadCount = getThreadCount(threadCount); + ExecutorService executorService = newFixedThreadPool(tomcatThreadCount); + return new Nio2EndPoint(executorService); + } + + private int getThreadCount(Integer threadCount) { + if (threadCount == null || threadCount == 0) { + return DEFAULT_THREAD_COUNT; + } + if (threadCount < 0) { + throw new IllegalArgumentException("์˜ฌ๋ฐ”๋ฅธ ์“ฐ๋ ˆ๋“œ ๊ฐœ์ˆ˜๋ฅผ ๋„ฃ์–ด์ฃผ์„ธ์š”."); + } + return threadCount; + } + + public void run() { + acceptor.run(); + } +} diff --git a/mvc/src/main/java/project/server/mvc/servlet/HttpServletRequest.java b/mvc/src/main/java/project/server/mvc/servlet/HttpServletRequest.java index 16495a8..23824df 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/HttpServletRequest.java +++ b/mvc/src/main/java/project/server/mvc/servlet/HttpServletRequest.java @@ -1,5 +1,6 @@ package project.server.mvc.servlet; +import project.server.mvc.servlet.http.Cookies; import project.server.mvc.servlet.http.HttpMethod; import project.server.mvc.servlet.http.RequestBody; import project.server.mvc.servlet.http.RequestLine; @@ -21,4 +22,8 @@ public interface HttpServletRequest extends ServletRequest { RequestBody getRequestBody(); String getAttribute(String key); + + Cookies getCookies(); + + String getHeader(String key); } diff --git a/mvc/src/main/java/project/server/mvc/servlet/HttpServletResponse.java b/mvc/src/main/java/project/server/mvc/servlet/HttpServletResponse.java index 8b87ae4..44a42d3 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/HttpServletResponse.java +++ b/mvc/src/main/java/project/server/mvc/servlet/HttpServletResponse.java @@ -1,9 +1,19 @@ package project.server.mvc.servlet; +import java.nio.channels.SocketChannel; +import project.server.mvc.servlet.http.Cookie; import project.server.mvc.servlet.http.HttpStatus; public interface HttpServletResponse extends ServletResponse { String getStatusAsString(); void setStatus(HttpStatus status); + + void addCookie(Cookie cookie); + + String getCookiesAsString(); + + SocketChannel getSocketChannel(); + + HttpStatus getStatus(); } diff --git a/mvc/src/main/java/project/server/mvc/servlet/Request.java b/mvc/src/main/java/project/server/mvc/servlet/Request.java index 79fb315..a0628e8 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/Request.java +++ b/mvc/src/main/java/project/server/mvc/servlet/Request.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import project.server.mvc.servlet.http.Cookies; import project.server.mvc.servlet.http.HttpHeader; import project.server.mvc.servlet.http.HttpHeaders; import project.server.mvc.servlet.http.HttpMethod; @@ -25,6 +26,16 @@ public Request(BufferedReader bufferedReader) throws IOException { this.requestBody = parseRequestBody(bufferedReader); } + public Request( + RequestLine requestLine, + HttpHeaders headers, + RequestBody requestBody + ) { + this.requestLine = requestLine; + this.headers = headers; + this.requestBody = requestBody; + } + private RequestLine parseRequestLine(BufferedReader bufferedReader) throws IOException { return new RequestLine(bufferedReader.readLine()); } @@ -83,7 +94,7 @@ public HttpMethod getMethod() { @Override public String getRequestUri() { - return requestLine.getRequestUrl(); + return requestLine.getRequestUri(); } @Override @@ -96,6 +107,16 @@ public String getAttribute(String key) { return requestBody.getAttribute(key); } + @Override + public Cookies getCookies() { + return headers.getCookies(); + } + + @Override + public String getHeader(String key) { + return headers.getHeaderValue(key); + } + @Override public String toString() { return String.format("%s%s\r\n%s", requestLine, headers, requestBody); diff --git a/mvc/src/main/java/project/server/mvc/servlet/Response.java b/mvc/src/main/java/project/server/mvc/servlet/Response.java index 0c07b55..498aa00 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/Response.java +++ b/mvc/src/main/java/project/server/mvc/servlet/Response.java @@ -1,18 +1,33 @@ package project.server.mvc.servlet; import java.io.OutputStream; +import java.nio.channels.SocketChannel; +import project.server.mvc.servlet.http.Cookie; +import project.server.mvc.servlet.http.HttpHeaders; import project.server.mvc.servlet.http.HttpStatus; +import static project.server.mvc.servlet.http.HttpStatus.OK; public class Response implements HttpServletResponse { private HttpStatus status; + private final HttpHeaders headers; + private final SocketChannel socketChannel; private final OutputStream outputStream; - public Response(OutputStream outputStream) { - this.status = HttpStatus.OK; + public Response( + SocketChannel socketChannel, + OutputStream outputStream + ) { + this.socketChannel = socketChannel; + this.status = OK; + this.headers = new HttpHeaders(); this.outputStream = outputStream; } + public Response(OutputStream outputStream) { + this(null, outputStream); + } + @Override public OutputStream getOutputStream() { return outputStream; @@ -27,4 +42,24 @@ public String getStatusAsString() { public void setStatus(HttpStatus status) { this.status = status; } + + @Override + public void addCookie(Cookie cookie) { + this.headers.addCookie(cookie); + } + + @Override + public String getCookiesAsString() { + return headers.getCookiesAsString(); + } + + @Override + public SocketChannel getSocketChannel() { + return socketChannel; + } + + @Override + public HttpStatus getStatus() { + return status; + } } diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/Cookie.java b/mvc/src/main/java/project/server/mvc/servlet/http/Cookie.java new file mode 100644 index 0000000..ddc9a6e --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/servlet/http/Cookie.java @@ -0,0 +1,12 @@ +package project.server.mvc.servlet.http; + +public record Cookie( + String name, + String value +) { + + @Override + public String toString() { + return String.format("name:%s=value:%s; ", name, value); + } +} diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/Cookies.java b/mvc/src/main/java/project/server/mvc/servlet/http/Cookies.java index 8e8da14..9ec3ea8 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/http/Cookies.java +++ b/mvc/src/main/java/project/server/mvc/servlet/http/Cookies.java @@ -1,6 +1,5 @@ package project.server.mvc.servlet.http; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -10,39 +9,62 @@ public class Cookies { private static final int KEY = 0; private static final int VALUE = 1; private static final String DELIMITER = "="; - private static final String COOKIE_DELIMITER = ", "; + private static final String COOKIE_DELIMITER = "; "; private static final String CARRIAGE_RETURN = "\r\n"; - private final List cookies; - private final Map cookiesMap = new HashMap<>(); + private final Map cookiesMap; public static final Cookies emptyCookies = new Cookies(); public Cookies() { - this.cookies = new ArrayList<>(); + this.cookiesMap = new HashMap<>(); } public Cookies(List cookieValues) { - this.cookies = cookieValues; - initCookieMap(cookies); + this.cookiesMap = new HashMap<>(); + initCookieMap(cookieValues); } private void initCookieMap(List cookies) { for (String cookie : cookies) { - String[] pair = cookie.split(DELIMITER); - cookiesMap.put(pair[KEY], pair[VALUE]); + String[] cookiePairs = cookie.split(COOKIE_DELIMITER); + for (String pair : cookiePairs) { + String[] keyValue = pair.trim().split(DELIMITER, 2); + if (keyValue.length == 2) { + String key = keyValue[KEY]; + String value = keyValue[VALUE]; + cookiesMap.put(key, new Cookie(key, value)); + } + } } } public boolean isEmpty() { - return this.cookies.isEmpty(); + return this.cookiesMap.isEmpty(); } - public String getValue(String name) { + public Map getCookiesMap() { + return cookiesMap; + } + + public Cookie getValue(String name) { return this.cookiesMap.get(name); } + public void add(Cookie cookie) { + this.cookiesMap.put(cookie.name(), cookie); + } + @Override public String toString() { - return String.format("%s", String.join(COOKIE_DELIMITER, cookies)) + CARRIAGE_RETURN; + StringBuilder stringBuilder = new StringBuilder(); + for (String key : cookiesMap.keySet()) { + if (!stringBuilder.isEmpty()) { + stringBuilder.append(COOKIE_DELIMITER); + } + stringBuilder.append(key) + .append(DELIMITER) + .append(cookiesMap.get(key).value()); + } + return stringBuilder.toString().trim() + CARRIAGE_RETURN; } } diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeader.java b/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeader.java index d48cf1e..240de7e 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeader.java +++ b/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeader.java @@ -25,8 +25,12 @@ public String getValue() { @Override public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } HttpHeader that = (HttpHeader) object; return name.equals(that.name) && value.equals(that.value); } diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeaders.java b/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeaders.java index e29e2ca..def2c27 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeaders.java +++ b/mvc/src/main/java/project/server/mvc/servlet/http/HttpHeaders.java @@ -75,6 +75,14 @@ public List getValue(String key) { return headers.get(key); } + public String getHeaderValue(String key) { + return headers.get(key).stream() + .filter(headerKey -> headerKey.getName().equals(key)) + .findAny() + .map(HttpHeader::getValue) + .orElseGet(() -> null); + } + public int getContentLength() { List headers = this.headers.getOrDefault( CONTENT_LENGTH, List.of(new HttpHeader(CONTENT_LENGTH, "0")) @@ -82,6 +90,18 @@ public int getContentLength() { return Integer.parseInt(headers.get(0).getValue()); } + public void addCookie(Cookie cookie) { + this.cookies.add(cookie); + } + + public String getCookiesAsString() { + return cookies.toString(); + } + + public Cookies getCookies() { + return cookies; + } + @Override public String toString() { StringBuilder stringBuilder = new StringBuilder(); diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/HttpStatus.java b/mvc/src/main/java/project/server/mvc/servlet/http/HttpStatus.java index dd6e7b6..50c4500 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/http/HttpStatus.java +++ b/mvc/src/main/java/project/server/mvc/servlet/http/HttpStatus.java @@ -2,9 +2,12 @@ public enum HttpStatus { OK("200 OK", 200), + NO_CONTENT("204 No Content", 204), MOVE_PERMANENTLY("301 Moved Permanently", 301), - BAD_REQUEST("BAD_REQUEST", 400), - NOT_FOUND("NOT_FOUND", 404); + BAD_REQUEST("400 Bad Request", 400), + NOT_FOUND("NOT_FOUND", 404), + UN_AUTHORIZED("401 Unauthorized", 401), + INTERNAL_SERVER_ERROR("500 Internal Server Error", 500); private final String status; private final int statusCode; diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/RequestBody.java b/mvc/src/main/java/project/server/mvc/servlet/http/RequestBody.java index a2116a8..9d938c2 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/http/RequestBody.java +++ b/mvc/src/main/java/project/server/mvc/servlet/http/RequestBody.java @@ -13,7 +13,7 @@ public class RequestBody { private final Map attributes; public RequestBody(String body) { - if (body == null) { + if (body == null || body.isBlank()) { attributes = null; return; } diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/RequestLine.java b/mvc/src/main/java/project/server/mvc/servlet/http/RequestLine.java index e85bc6d..8adfeb9 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/http/RequestLine.java +++ b/mvc/src/main/java/project/server/mvc/servlet/http/RequestLine.java @@ -2,6 +2,7 @@ import java.net.URLConnection; import java.util.Objects; +import static project.server.mvc.servlet.http.HttpMethod.findHttpMethod; import static project.server.mvc.servlet.http.HttpVersion.findHttpVersion; public class RequestLine { @@ -12,39 +13,61 @@ public class RequestLine { private static final String DELIMITER = " "; private static final String BASIC_PREFIX = "/"; private static final String STATIC_HOME = "index.html"; + private static final String OCTET_STREAM = "application/octet-stream"; private final HttpMethod httpMethod; - private final RequestUri requestURI; + private final RequestUri requestUri; private final HttpVersion httpVersion; private String contentType; public RequestLine(String startLine) { String[] startLineArray = startLine.split(DELIMITER); - this.httpMethod = HttpMethod.findHttpMethod(startLineArray[METHOD]); - this.requestURI = getRequestUrl(startLine); + this.httpMethod = findHttpMethod(startLineArray[METHOD]); + this.requestUri = getRequestUri(startLine); this.httpVersion = findHttpVersion(startLineArray[VERSION]); } - private RequestUri getRequestUrl(String startLine) { + private RequestUri getRequestUri(String startLine) { String[] startLineArray = startLine.split(DELIMITER); - String requestFile = null; + RequestUri requestUri = getRequestUri(startLine, startLineArray); + if (requestUri != null) { + return requestUri; + } + return new RequestUri(startLineArray[URI]); + } + + private RequestUri getRequestUri( + String startLine, + String[] startLineArray + ) { + String requestFile; if (!startLine.isEmpty()) { - if (startLineArray.length >= 2) { - requestFile = startLineArray[1].substring(1); - if (requestFile.isEmpty()) { - requestFile = STATIC_HOME; - } - } + requestFile = getRequestFile(startLineArray); this.contentType = getContentType(requestFile); return new RequestUri(BASIC_PREFIX + requestFile); } - return new RequestUri(startLineArray[URI]); + return null; + } + + private String getRequestFile(String[] startLineArray) { + String resultFile = null; + if (isStaticResource(startLineArray)) { + resultFile = startLineArray[URI].substring(1); + if (resultFile.isEmpty()) { + resultFile = STATIC_HOME; + } + } + return resultFile; + } + + private boolean isStaticResource(String[] startLineArray) { + return startLineArray.length >= 2; } private String getContentType(String filePath) { String contentType = URLConnection.guessContentTypeFromName(filePath); if (contentType == null) { - contentType = "application/octet-stream"; + contentType = OCTET_STREAM; } return contentType; } @@ -53,8 +76,8 @@ public HttpMethod getHttpMethod() { return this.httpMethod; } - public String getRequestUrl() { - return requestURI.url(); + public String getRequestUri() { + return requestUri.url(); } public String getContentType() { @@ -63,12 +86,16 @@ public String getContentType() { @Override public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } RequestLine that = (RequestLine) object; - return httpMethod == that.httpMethod && - requestURI.equals(that.requestURI) && - httpVersion == that.httpVersion; + return httpMethod == that.httpMethod + && requestUri.equals(that.requestUri) + && httpVersion == that.httpVersion; } public String getHttpVersionAsString() { @@ -82,6 +109,6 @@ public int hashCode() { @Override public String toString() { - return String.format("%s %s %s\r\n", httpMethod, requestURI, httpVersion.getValue()); + return String.format("%s %s %s\r\n", httpMethod, requestUri, httpVersion.getValue()); } } diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/ResponseBody.java b/mvc/src/main/java/project/server/mvc/servlet/http/ResponseBody.java index 6997c29..384b27d 100644 --- a/mvc/src/main/java/project/server/mvc/servlet/http/ResponseBody.java +++ b/mvc/src/main/java/project/server/mvc/servlet/http/ResponseBody.java @@ -21,8 +21,12 @@ public String getBody() { @Override public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } ResponseBody that = (ResponseBody) object; return body.equals(that.body); } diff --git a/mvc/src/main/java/project/server/mvc/servlet/http/StatusLine.java b/mvc/src/main/java/project/server/mvc/servlet/http/StatusLine.java new file mode 100644 index 0000000..8fa67a3 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/servlet/http/StatusLine.java @@ -0,0 +1,33 @@ +package project.server.mvc.servlet.http; + +public class StatusLine { + + private static final HttpVersion basicProtocolVersion = HttpVersion.HTTP_1_1; + + private String protocolVersion; + private HttpStatus httpStatus; + + public StatusLine() { + this.protocolVersion = basicProtocolVersion.getValue(); + } + + public StatusLine(HttpStatus httpStatus) { + this.httpStatus = httpStatus; + } + + public String getProtocolVersion() { + return protocolVersion; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } + + public void setStatus(HttpStatus status) { + this.httpStatus = status; + } + + public String getHttpStatusAsString() { + return httpStatus.getStatus(); + } +} diff --git a/mvc/src/main/java/project/server/mvc/springframework/annotation/GetMapping.java b/mvc/src/main/java/project/server/mvc/springframework/annotation/GetMapping.java index dc4677b..f5dd177 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/annotation/GetMapping.java +++ b/mvc/src/main/java/project/server/mvc/springframework/annotation/GetMapping.java @@ -8,7 +8,7 @@ import project.server.mvc.servlet.http.HttpMethod; @Documented -@Target(ElementType.METHOD) +@Target({ElementType.TYPE_USE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @RequestMapping(method = HttpMethod.GET) public @interface GetMapping { diff --git a/mvc/src/main/java/project/server/mvc/springframework/context/Application.java b/mvc/src/main/java/project/server/mvc/springframework/context/Application.java deleted file mode 100644 index c5dcace..0000000 --- a/mvc/src/main/java/project/server/mvc/springframework/context/Application.java +++ /dev/null @@ -1,64 +0,0 @@ -package project.server.mvc.springframework.context; - -import java.net.ServerSocket; -import java.net.Socket; -import java.util.concurrent.ExecutorService; -import static java.util.concurrent.Executors.newFixedThreadPool; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import project.server.mvc.springframework.handler.RequestHandler; - -public class Application { - - private static final int FIXED_THREAD_COUNTS = 32; - private static final int MIN_PORT_NUMBER = 1; - private static final int MAX_PORT_NUMBER = 65535; - private static final int DEFAULT_PORT = 8085; - - private static final Logger log = LoggerFactory.getLogger(Application.class); - private static final ExecutorService executors = newFixedThreadPool(FIXED_THREAD_COUNTS); - - static { - ApplicationContext context = null; - try { - context = new ApplicationContext("project"); - } catch (Exception exception) { - throw new RuntimeException(exception); - } - ApplicationContextProvider provider = new ApplicationContextProvider(); - provider.setApplicationContext(context); - } - - public static void run(String[] args) { - int port = findPort(args); - try (ServerSocket listenSocket = new ServerSocket(port)) { - log.info("Web Application Server started {} port.", port); - Socket connection; - while ((connection = listenSocket.accept()) != null) { - new RequestHandler(connection).start(); - executors.submit(new RequestHandler(connection)); - } - } catch (Exception exception) { - throw new RuntimeException(exception); - } - } - - private static int findPort(String[] args) { - if (args == null || args.length == 0) { - return DEFAULT_PORT; - } - return parsePort(args); - } - - private static int parsePort(String[] args) { - try { - int port = Integer.parseInt(args[0]); - if (port < MIN_PORT_NUMBER || port > MAX_PORT_NUMBER) { - return DEFAULT_PORT; - } - return port; - } catch (NumberFormatException exception) { - return DEFAULT_PORT; - } - } -} diff --git a/mvc/src/main/java/project/server/mvc/springframework/handler/RequestHandler.java b/mvc/src/main/java/project/server/mvc/springframework/handler/RequestHandler.java index 23e3ae7..b00e47d 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/handler/RequestHandler.java +++ b/mvc/src/main/java/project/server/mvc/springframework/handler/RequestHandler.java @@ -7,8 +7,7 @@ import java.io.OutputStream; import java.net.Socket; import java.nio.charset.StandardCharsets; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; import project.server.mvc.servlet.Request; @@ -16,10 +15,9 @@ import project.server.mvc.servlet.Servlet; import static project.server.mvc.springframework.context.ApplicationContextProvider.getBean; +@Slf4j public final class RequestHandler extends Thread { - private static final Logger log = LoggerFactory.getLogger(RequestHandler.class); - private final Socket connection; private final Servlet dispatcherServlet; diff --git a/mvc/src/main/java/project/server/mvc/springframework/handler/RequestMappingInfo.java b/mvc/src/main/java/project/server/mvc/springframework/handler/RequestMappingInfo.java index 9fb8557..8c55b39 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/handler/RequestMappingInfo.java +++ b/mvc/src/main/java/project/server/mvc/springframework/handler/RequestMappingInfo.java @@ -9,10 +9,14 @@ public record RequestMappingInfo( ) { @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RequestMappingInfo that = (RequestMappingInfo) o; + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + RequestMappingInfo that = (RequestMappingInfo) object; return httpMethod == that.httpMethod && url.equals(that.url); } diff --git a/mvc/src/main/java/project/server/mvc/springframework/ui/ModelMap.java b/mvc/src/main/java/project/server/mvc/springframework/ui/ModelMap.java index b213d99..1333466 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/ui/ModelMap.java +++ b/mvc/src/main/java/project/server/mvc/springframework/ui/ModelMap.java @@ -3,20 +3,18 @@ import java.util.LinkedHashMap; import java.util.Map; -public class ModelMap extends LinkedHashMap { +public class ModelMap { - public ModelMap addAttribute( + private final Map map = new LinkedHashMap(); + + public void put( String attributeName, Object attributeValue ) { - put(attributeName, attributeValue); - return this; + map.put(attributeName, attributeValue); } - public ModelMap addAllAttributes(Map attributes) { - if (attributes != null) { - putAll(attributes); - } - return this; + public Object getAttribute(String key) { + return map.get(key); } } diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/method/HandlerMethod.java b/mvc/src/main/java/project/server/mvc/springframework/web/method/HandlerMethod.java index e56eba8..7756f8e 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/method/HandlerMethod.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/method/HandlerMethod.java @@ -57,10 +57,14 @@ public boolean handleStaticResource() { } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - HandlerMethod that = (HandlerMethod) o; + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + HandlerMethod that = (HandlerMethod) object; return handler.equals(that.handler); } diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/BeanNameViewResolver.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/BeanNameViewResolver.java index 1230909..390437d 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/BeanNameViewResolver.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/BeanNameViewResolver.java @@ -6,13 +6,15 @@ public class BeanNameViewResolver implements ViewResolver { private static final String HOME = "index.html"; - private static final String REDIRECT_LOCATION = "redirect:/index.html"; + private static final String MY_INFO = "/my-info.html"; + private static final String REDIRECT_VIEW = "redirect:/index.html"; private final Map views = new HashMap<>(); public BeanNameViewResolver() { views.put(HOME, new StaticView()); - views.put(REDIRECT_LOCATION, new RedirectView()); + views.put(MY_INFO, new MyInfoView()); + views.put(REDIRECT_VIEW, new RedirectView()); } @Override diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/DispatcherServlet.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/DispatcherServlet.java index 59e5b3e..b97c77a 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/DispatcherServlet.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/DispatcherServlet.java @@ -1,22 +1,24 @@ package project.server.mvc.springframework.web.servlet; import java.util.List; -import project.server.mvc.springframework.web.servlet.mvc.method.RequestMappingHandlerMapping; -import project.server.mvc.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; -import project.server.mvc.servlet.ServletException; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; +import project.server.mvc.servlet.ServletException; +import project.server.mvc.springframework.web.servlet.mvc.method.RequestMappingHandlerMapping; +import project.server.mvc.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; public class DispatcherServlet extends FrameworkServlet { private final List handlerMappings; private final List handlerAdapters; private final List viewResolvers; + private final GlobalExceptionHandler exceptionHandler; public DispatcherServlet() { this.handlerMappings = List.of(new RequestMappingHandlerMapping()); this.handlerAdapters = List.of(new RequestMappingHandlerAdapter()); this.viewResolvers = List.of(new BeanNameViewResolver()); + this.exceptionHandler = new GlobalExceptionHandler(); } @Override @@ -36,14 +38,20 @@ private void doDispatch( HttpServletRequest request, HttpServletResponse response ) throws Exception { - HandlerExecutionChain handler = getHandler(request); - if (handler == null) { - return; - } + try { + HandlerExecutionChain handler = getHandler(request); + if (handler == null) { + return; + } - HandlerAdapter handlerAdapter = getHandlerAdapter(handler.getHandler()); - ModelAndView modelAndView = handlerAdapter.handle(request, response, handler.getHandler()); - processDispatchResult(request, response, modelAndView); + HandlerAdapter handlerAdapter = getHandlerAdapter(handler.getHandler()); + ModelAndView modelAndView = handlerAdapter.handle(request, response, handler.getHandler()); + processDispatchResult(request, response, modelAndView); + } catch (Exception exception) { + exceptionHandler.resolveException(response, exception); + StaticView view = new StaticView(); + view.render(null, request, response); + } } private HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/FrameworkServlet.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/FrameworkServlet.java index aa792d5..e5d6498 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/FrameworkServlet.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/FrameworkServlet.java @@ -1,16 +1,18 @@ package project.server.mvc.springframework.web.servlet; import java.io.IOException; -import project.server.mvc.springframework.web.method.HandlerMethod; -import project.server.mvc.springframework.web.servlet.resource.ResourceHttpRequestHandler; +import java.util.List; import project.server.mvc.servlet.HttpServletBean; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; +import project.server.mvc.springframework.web.method.HandlerMethod; +import project.server.mvc.springframework.web.servlet.resource.ResourceHttpRequestHandler; public abstract class FrameworkServlet extends HttpServletBean { private static final HandlerMethod staticResourceHandlerMethod = new HandlerMethod(new ResourceHttpRequestHandler()); private static final String STATIC_RESOURCE = "."; + private static final List excludeStaticResources = List.of("my-info.html"); @Override public void init() { @@ -35,8 +37,21 @@ public void doGet( } private boolean isStaticResource(HttpServletRequest request) { - String url = request.getRequestUri(); - return url.contains(STATIC_RESOURCE); + String uri = request.getRequestUri(); + String[] parsedUri = uri.split("/"); + boolean uriContainsExclude = false; + for (String eachUri : parsedUri) { + if (".css".equals(eachUri) || ".png".equals(eachUri) || ".favicon".equals(eachUri)) { + return true; + } + } + for (String eachUri : parsedUri) { + if (excludeStaticResources.contains(eachUri)) { + uriContainsExclude = true; + break; + } + } + return uri.contains(STATIC_RESOURCE) && !uriContainsExclude; } private void processStaticRequest( diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/GlobalExceptionHandler.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/GlobalExceptionHandler.java new file mode 100644 index 0000000..060ed4b --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/GlobalExceptionHandler.java @@ -0,0 +1,41 @@ +package project.server.mvc.springframework.web.servlet; + +import lombok.extern.slf4j.Slf4j; +import project.server.mvc.servlet.HttpServletResponse; +import project.server.mvc.servlet.http.HttpStatus; +import static project.server.mvc.servlet.http.HttpStatus.UN_AUTHORIZED; +import project.server.mvc.springframework.annotation.Component; + +@Slf4j +@Component +public class GlobalExceptionHandler { + + public void resolveException( + HttpServletResponse response, + Exception exception + ) { + Throwable cause = exception.getCause(); + HttpStatus findStatus = getHttpStatus(cause.getMessage()); + log.error("{code:{}, message:{}}", findStatus.getStatusCode(), cause.getMessage()); + response.setStatus(findStatus); + } + + public HttpStatus getHttpStatus(String message) { + if ("์ค‘๋ณต๋œ ์•„์ด๋”” ์ž…๋‹ˆ๋‹ค.".equals(message)) { + return HttpStatus.BAD_REQUEST; + } + if ("์˜ฌ๋ฐ”๋ฅธ ๊ฐ’์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.".equals(message)) { + return HttpStatus.BAD_REQUEST; + } + if ("์ด๋ฏธ ๊ฐ€์ž…๋œ ์‚ฌ์šฉ์ž ์ž…๋‹ˆ๋‹ค.".equals(message)) { + return HttpStatus.BAD_REQUEST; + } + if ("์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.".equals(message)) { + return HttpStatus.NOT_FOUND; + } + if ("๊ถŒํ•œ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.".equals(message)) { + return UN_AUTHORIZED; + } + return HttpStatus.INTERNAL_SERVER_ERROR; + } +} diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/HandlerExecutionChain.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/HandlerExecutionChain.java index 7fb54f2..9d8f858 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/HandlerExecutionChain.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/HandlerExecutionChain.java @@ -16,8 +16,12 @@ public Object getHandler() { @Override public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } HandlerExecutionChain that = (HandlerExecutionChain) object; return handler.equals(that.handler); } diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/ModelAndView.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/ModelAndView.java index d8c1393..b5b5197 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/ModelAndView.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/ModelAndView.java @@ -10,14 +10,20 @@ public class ModelAndView { public ModelAndView() { } - public ModelAndView(Object view) { + public ModelAndView( + Object view, + ModelMap model + ) { this.view = view; - this.model = new ModelMap(); + this.model = model; + } + + public ModelAndView(Object view) { + this(view, null); } public ModelAndView(String viewName) { - this.view = viewName; - this.model = new ModelMap(); + this(viewName, null); } public Object getView() { diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/MyInfoView.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/MyInfoView.java new file mode 100644 index 0000000..c9ba5ae --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/MyInfoView.java @@ -0,0 +1,102 @@ +package project.server.mvc.springframework.web.servlet; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import static java.nio.charset.StandardCharsets.UTF_8; +import project.server.mvc.servlet.HttpServletRequest; +import project.server.mvc.servlet.HttpServletResponse; +import project.server.mvc.springframework.ui.ModelMap; + +public class MyInfoView implements View { + + private static final String CARRIAGE_RETURN = "\r\n"; + private static final int BASE_OFFSET = 0; + private static final int DEFAULT_BUFFER_SIZE = 1_024; + private static final int EMPTY = -1; + private static final String CONTENT_LENGTH = "Content-Length: "; + private static final String CONTENT_TYPE = "Content-Type: "; + private static final String DELIMITER = " "; + private static final String MASKING = "*"; + private static final String STATIC_PREFIX = "static"; + + @Override + public void render( + ModelAndView modelAndView, + HttpServletRequest request, + HttpServletResponse response + ) throws Exception { + ModelMap modelMap = modelAndView.getModelMap(); + InputStream inputStream = getInputStream(STATIC_PREFIX + request.getRequestUri()); + byte[] buffer = readStream(inputStream, modelMap); + setResponseHeader(request, response, buffer.length); + responseBody(response, buffer); + } + + private InputStream getInputStream(String path) { + return getClass().getClassLoader().getResourceAsStream(path); + } + + private void responseBody( + HttpServletResponse response, + byte[] body + ) throws IOException { + SocketChannel channel = response.getSocketChannel(); + ByteBuffer buffer = ByteBuffer.wrap(body); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + } + + private void setResponseHeader( + HttpServletRequest request, + HttpServletResponse response, + int lengthOfBodyContent + ) throws IOException { + SocketChannel channel = response.getSocketChannel(); + String header = request.getHttpVersion() + DELIMITER + getStatus(response) + CARRIAGE_RETURN + + CONTENT_TYPE + + request.getContentType() + + CARRIAGE_RETURN + + CONTENT_LENGTH + + lengthOfBodyContent + + CARRIAGE_RETURN + + CARRIAGE_RETURN; + ByteBuffer headerBuffer = ByteBuffer.wrap(header.getBytes(UTF_8)); + while (headerBuffer.hasRemaining()) { + channel.write(headerBuffer); + } + } + + private static String getStatus(HttpServletResponse response) { + return response.getStatusAsString() + DELIMITER; + } + + private byte[] readStream( + InputStream inputStream, + ModelMap modelMap + ) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != EMPTY) { + byteArrayOutputStream.write(buffer, BASE_OFFSET, bytesRead); + } + String convertedHtml = getConvertedHtml(modelMap, byteArrayOutputStream); + return convertedHtml.getBytes(UTF_8); + } + + private static String getConvertedHtml( + ModelMap modelMap, + ByteArrayOutputStream byteArrayOutputStream + ) { + Object username = modelMap.getAttribute("username"); + Object password = modelMap.getAttribute("password"); + + String htmlPage = byteArrayOutputStream.toString(UTF_8); + String replacedUsernameHtml = htmlPage.replace(MASKING, username.toString()); + return replacedUsernameHtml.replace("\\", MASKING.repeat(password.toString().length())); + } +} diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/RedirectView.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/RedirectView.java index ee64dee..f948dff 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/RedirectView.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/RedirectView.java @@ -1,12 +1,13 @@ package project.server.mvc.springframework.web.servlet; -import java.io.DataOutputStream; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; -import static project.server.mvc.servlet.http.HttpStatus.MOVE_PERMANENTLY; public class RedirectView implements View { @@ -17,9 +18,8 @@ public class RedirectView implements View { private static final String REDIRECT_LOCATION = "redirect:/index.html"; private static final String HOME = "/index.html"; - private final Map views = new HashMap<>(); - public RedirectView() { + Map views = new HashMap<>(); views.put(REDIRECT_LOCATION, new StaticView()); } @@ -29,7 +29,6 @@ public void render( HttpServletRequest request, HttpServletResponse response ) throws Exception { - response.setStatus(MOVE_PERMANENTLY); response(request, response); } @@ -40,16 +39,17 @@ private void response( setResponseHeader(request, response); } - private void setResponseHeader( - HttpServletRequest request, - HttpServletResponse response - ) throws IOException { - DataOutputStream dos = new DataOutputStream(response.getOutputStream()); - dos.writeBytes(getStartLine(request, response)); - dos.writeBytes(LOCATION_DELIMITER + getRedirectLocation(request) + CARRIAGE_RETURN); - dos.writeBytes(CARRIAGE_RETURN); - dos.flush(); - dos.close(); + private void setResponseHeader(HttpServletRequest request, HttpServletResponse response) throws IOException { + SocketChannel channel = response.getSocketChannel(); + String header = getStartLine(request, response) + + LOCATION_DELIMITER + getRedirectLocation(request) + CARRIAGE_RETURN + + "Access-Control-Allow-Origin: *" + CARRIAGE_RETURN + + "Set-Cookie: " + response.getCookiesAsString() + CARRIAGE_RETURN + + CARRIAGE_RETURN; + ByteBuffer headerBuffer = ByteBuffer.wrap(header.getBytes(StandardCharsets.UTF_8)); + while (headerBuffer.hasRemaining()) { + channel.write(headerBuffer); + } } private String getStartLine( diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/StaticView.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/StaticView.java index df80f29..66f0c11 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/StaticView.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/StaticView.java @@ -1,88 +1,101 @@ package project.server.mvc.springframework.web.servlet; -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; -import java.net.URLConnection; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import static java.nio.charset.StandardCharsets.UTF_8; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; +import project.server.mvc.servlet.http.HttpStatus; +import static project.server.mvc.servlet.http.HttpStatus.UN_AUTHORIZED; public class StaticView implements View { private static final String CARRIAGE_RETURN = "\r\n"; - private static final String STATIC_PREFIX = "static/"; + private static final String INVALID_COOKIE = "Set-Cookie=; Max-Age=0; Path=/"; + private static final String DELIMITER = " "; + private static final String CONTENT_TYPE = "Content-Type: "; + private static final String TEXT_HTML = "text/html"; + private static final String HTML_REQUEST_LINE = CONTENT_TYPE + TEXT_HTML + CARRIAGE_RETURN + CARRIAGE_RETURN; + private static final String INDEX_HTML = "static/index.html"; + private static final String NEXT_LINE = "\n"; - public InputStream getInputStream(String path) { - return getClass().getClassLoader() - .getResourceAsStream(path); + @Override + public void render( + ModelAndView modelAndView, + HttpServletRequest request, + HttpServletResponse response + ) throws Exception { + response(request, response); } private void response( HttpServletRequest request, - DataOutputStream dataOutputStream, - InputStream inputStream + HttpServletResponse response ) throws IOException { - String contentType = getContentType(request.getRequestUri()); - byte[] buffer = readStream(inputStream); - response200Header(dataOutputStream, buffer.length, contentType); - responseBody(dataOutputStream, buffer); + setResponseHeader(request, response); } - private void responsePageNotFound() { + private void setResponseHeader( + HttpServletRequest request, + HttpServletResponse response + ) throws IOException { + SocketChannel channel = response.getSocketChannel(); + HttpStatus httpStatus = response.getStatus(); + String header = getHeader(request, response); + + ByteBuffer headerBuffer = ByteBuffer.wrap(header.getBytes(UTF_8)); + channel.write(headerBuffer); + + if (UN_AUTHORIZED.equals(httpStatus)) { + InputStream inputStream = getInputStream(); + String htmlContent = readInputStream(inputStream); + ByteBuffer contentTypeBuffer = ByteBuffer.wrap(HTML_REQUEST_LINE.getBytes(UTF_8)); + channel.write(contentTypeBuffer); + ByteBuffer htmlBuffer = ByteBuffer.wrap(htmlContent.getBytes(UTF_8)); + channel.write(htmlBuffer); + } } - private boolean isStaticPage(HttpServletRequest request) { - String url = request.getRequestUri(); - return url.contains("html"); + private InputStream getInputStream() { + return getClass().getClassLoader() + .getResourceAsStream(INDEX_HTML); } - @Override - public void render( - ModelAndView modelAndView, + private String getHeader( HttpServletRequest request, HttpServletResponse response - ) throws Exception { -// res - } + ) { + HttpStatus httpStatus = response.getStatus(); - private void response200Header( - OutputStream outputStream, - int lengthOfBodyContent, - String contentType - ) throws IOException { - DataOutputStream dos = new DataOutputStream(outputStream); - dos.writeBytes("HTTP/1.1 200 OK " + CARRIAGE_RETURN); - dos.writeBytes("Content-Type: " + contentType + CARRIAGE_RETURN); - dos.writeBytes("Content-Length: " + lengthOfBodyContent + CARRIAGE_RETURN); - dos.writeBytes(CARRIAGE_RETURN); - } + StringBuilder headerBuilder = new StringBuilder(); + headerBuilder.append(request.getHttpVersion()) + .append(DELIMITER) + .append(httpStatus.getStatusCode()) + .append(DELIMITER) + .append(httpStatus.getStatus()) + .append(CARRIAGE_RETURN); - private byte[] readStream(InputStream inputStream) throws IOException { - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) != -1) { - byteArrayOutputStream.write(buffer, 0, bytesRead); + if (UN_AUTHORIZED.equals(httpStatus)) { + headerBuilder.append(INVALID_COOKIE) + .append(CARRIAGE_RETURN); } - return byteArrayOutputStream.toByteArray(); - } - - private void responseBody( - OutputStream outputStream, - byte[] body - ) throws IOException { - outputStream.write(body); - outputStream.flush(); + return headerBuilder.toString(); } - private String getContentType(String filePath) { - String contentType = URLConnection.guessContentTypeFromName(filePath); - if (contentType == null) { - contentType = "application/octet-stream"; + private String readInputStream(InputStream inputStream) throws IOException { + StringBuilder stringBuilder = new StringBuilder(); + InputStreamReader inputStreamReader = new InputStreamReader(inputStream, UTF_8); + try (BufferedReader reader = new BufferedReader(inputStreamReader)) { + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line).append(NEXT_LINE); + } } - return contentType; + return stringBuilder.toString(); } } diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java index 544f69e..a0519d6 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java @@ -3,12 +3,11 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; -import project.server.mvc.springframework.context.ApplicationContext; -import project.server.mvc.springframework.web.method.HandlerMethod; -import project.server.mvc.springframework.web.servlet.resource.ResourceHttpRequestHandler; -import project.server.mvc.servlet.http.HttpMethod; import project.server.mvc.servlet.HttpServletRequest; +import project.server.mvc.servlet.http.HttpMethod; +import project.server.mvc.springframework.context.ApplicationContext; import project.server.mvc.springframework.handler.RequestMappingInfo; +import project.server.mvc.springframework.web.method.HandlerMethod; public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMapping { @@ -17,9 +16,6 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMappin @Override protected Object getHandlerInternal(HttpServletRequest request) throws Exception { String lookupPath = initLookupPath(request); - if (isStaticResource(lookupPath)) { - return new HandlerMethod(new ResourceHttpRequestHandler()); - } return lookupHandlerMethod(lookupPath, request); } @@ -27,11 +23,6 @@ private String initLookupPath(HttpServletRequest request) { return getRequestPath(request); } - private boolean isStaticResource(String lookupPath) { - return lookupPath != null && - (lookupPath.equals("/") || lookupPath.contains(".")); - } - private String getRequestPath(HttpServletRequest request) { return request.getRequestUri(); } @@ -53,15 +44,25 @@ public MappingRegistry() { this.registry = new HashMap<>(); Object homeController = Optional.ofNullable(ApplicationContext.getBean("HomeController")).get(); Object signUpController = Optional.ofNullable(ApplicationContext.getBean("SignUpController")).get(); + Object loginUpController = Optional.ofNullable(ApplicationContext.getBean("LoginController")).get(); + Object userInfoController = Optional.ofNullable(ApplicationContext.getBean("UserInfoSearchController")).get(); registry.put( new RequestMappingInfo(HttpMethod.GET, "/"), new MappingRegistration(new HandlerMethod(homeController)) ); + registry.put( + new RequestMappingInfo(HttpMethod.GET, "/my-info.html"), + new MappingRegistration(new HandlerMethod(userInfoController)) + ); registry.put( new RequestMappingInfo(HttpMethod.POST, "/sign-up"), new MappingRegistration(new HandlerMethod(signUpController)) ); + registry.put( + new RequestMappingInfo(HttpMethod.POST, "/sign-in"), + new MappingRegistration(new HandlerMethod(loginUpController)) + ); } public MappingRegistration getMappingRegistration(RequestMappingInfo requestMappingInfo) { diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java index 1dad6da..fd026af 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/mvc/method/AbstractHandlerMethodAdapter.java @@ -2,11 +2,11 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import project.server.mvc.springframework.web.servlet.HandlerAdapter; +import project.server.mvc.servlet.HttpServletRequest; +import project.server.mvc.servlet.HttpServletResponse; import project.server.mvc.springframework.web.method.HandlerMethod; +import project.server.mvc.springframework.web.servlet.HandlerAdapter; import project.server.mvc.springframework.web.servlet.ModelAndView; -import project.server.mvc.servlet.HttpServletResponse; -import project.server.mvc.servlet.HttpServletRequest; public abstract class AbstractHandlerMethodAdapter implements HandlerAdapter { @@ -31,9 +31,6 @@ private ModelAndView invokeHandlerMethod( ) throws IllegalAccessException, InvocationTargetException { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); - if (handlerMethod.handleStaticResource()) { - return new ModelAndView(); - } Object instance = handlerMethod.getHandler(); Object[] args = new Object[]{request, response}; diff --git a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index 11c4bdd..9ce2f32 100644 --- a/mvc/src/main/java/project/server/mvc/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/mvc/src/main/java/project/server/mvc/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -1,10 +1,11 @@ package project.server.mvc.springframework.web.servlet.resource; import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import static java.nio.charset.StandardCharsets.UTF_8; import project.server.mvc.servlet.HttpServletRequest; import project.server.mvc.servlet.HttpServletResponse; import static project.server.mvc.servlet.http.HttpStatus.NOT_FOUND; @@ -59,8 +60,7 @@ private String getFile(String uri) { } private InputStream getInputStream(String path) { - return getClass().getClassLoader() - .getResourceAsStream(path); + return getClass().getClassLoader().getResourceAsStream(path); } private void response( @@ -70,7 +70,7 @@ private void response( ) throws IOException { byte[] buffer = readStream(inputStream); setResponseHeader(request, response, buffer.length); - responseBody(response.getOutputStream(), buffer); + responseBody(response, buffer); } private void responsePageNotFound(HttpServletResponse response) { @@ -78,11 +78,14 @@ private void responsePageNotFound(HttpServletResponse response) { } private void responseBody( - OutputStream outputStream, + HttpServletResponse response, byte[] body ) throws IOException { - outputStream.write(body); - outputStream.flush(); + SocketChannel channel = response.getSocketChannel(); + ByteBuffer buffer = ByteBuffer.wrap(body); + while (buffer.hasRemaining()) { + channel.write(buffer); + } } private void setResponseHeader( @@ -90,11 +93,15 @@ private void setResponseHeader( HttpServletResponse response, int lengthOfBodyContent ) throws IOException { - DataOutputStream dos = new DataOutputStream(response.getOutputStream()); - dos.writeBytes(request.getHttpVersion() + getStatus(response) + CARRIAGE_RETURN); - dos.writeBytes(CONTENT_TYPE + request.getContentType() + CARRIAGE_RETURN); - dos.writeBytes(CONTENT_LENGTH + lengthOfBodyContent + CARRIAGE_RETURN); - dos.writeBytes(CARRIAGE_RETURN); + SocketChannel channel = response.getSocketChannel(); + String header = request.getHttpVersion() + DELIMITER + getStatus(response) + CARRIAGE_RETURN + + CONTENT_TYPE + request.getContentType() + CARRIAGE_RETURN + + CONTENT_LENGTH + lengthOfBodyContent + CARRIAGE_RETURN + + CARRIAGE_RETURN; + ByteBuffer headerBuffer = ByteBuffer.wrap(header.getBytes(UTF_8)); + while (headerBuffer.hasRemaining()) { + channel.write(headerBuffer); + } } private static String getStatus(HttpServletResponse response) { diff --git a/mvc/src/main/java/project/server/mvc/tomcat/AbstractEndpoint.java b/mvc/src/main/java/project/server/mvc/tomcat/AbstractEndpoint.java new file mode 100644 index 0000000..5fe0057 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/tomcat/AbstractEndpoint.java @@ -0,0 +1,14 @@ +package project.server.mvc.tomcat; + +import java.util.concurrent.ExecutorService; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class AbstractEndpoint { + + private final ExecutorService executorService; + + public AbstractEndpoint(ExecutorService executorService) { + this.executorService = executorService; + } +} diff --git a/mvc/src/main/java/project/server/mvc/tomcat/AsyncRequest.java b/mvc/src/main/java/project/server/mvc/tomcat/AsyncRequest.java new file mode 100644 index 0000000..50a2fa3 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/tomcat/AsyncRequest.java @@ -0,0 +1,92 @@ +package project.server.mvc.tomcat; + +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import project.server.mvc.servlet.HttpServletRequest; +import project.server.mvc.servlet.HttpServletResponse; +import project.server.mvc.servlet.Request; +import project.server.mvc.servlet.Response; +import project.server.mvc.servlet.http.HttpHeaders; +import project.server.mvc.servlet.http.RequestBody; +import project.server.mvc.servlet.http.RequestLine; +import static project.server.mvc.springframework.context.ApplicationContextProvider.getBean; +import project.server.mvc.springframework.web.servlet.DispatcherServlet; + +@Slf4j +public class AsyncRequest implements Runnable { + + private static final int START_OFFSET = 0; + private static final int START_LINE = 0; + private static final String CARRIAGE_RETURN = "\r\n"; + + private final SocketChannel socketChannel; + private final ByteBuffer buffer; + private final DispatcherServlet dispatcherServlet; + + public AsyncRequest( + SocketChannel socketChannel, + ByteBuffer buffer + ) { + this.socketChannel = socketChannel; + this.buffer = buffer; + this.dispatcherServlet = getBean("dispatcherServlet"); + } + + @Override + public void run() { + try { + String httpMessage = new String(buffer.array(), START_OFFSET, buffer.limit()); + String[] lines = httpMessage.split(CARRIAGE_RETURN); + + int index = 1; + List headerLines = new ArrayList<>(); + while (index < lines.length && !lines[index].isEmpty()) { + headerLines.add(lines[index]); + index++; + } + + String requestBody = getRequestBodyBuilder(lines, index); + + try (SocketChannel channel = this.socketChannel; + OutputStream outputStream = channel.socket().getOutputStream()) { + + HttpServletRequest request = createHttpServletRequest(lines, headerLines, requestBody); + HttpServletResponse response = new Response(channel, outputStream); + dispatcherServlet.service(request, response); + } + + } catch (Exception exception) { + log.error("message: {}", exception.getMessage()); + } + } + + private String getRequestBodyBuilder( + String[] lines, + int index + ) { + StringBuilder requestBodyBuilder = new StringBuilder(); + if (index < lines.length) { + for (int subIndex = index + 1; subIndex < lines.length; subIndex++) { + requestBodyBuilder.append(lines[subIndex]) + .append(CARRIAGE_RETURN); + } + } + return requestBodyBuilder.toString(); + } + + private HttpServletRequest createHttpServletRequest( + String[] lines, + List headerLines, + String requestBody + ) { + return new Request( + new RequestLine(lines[START_LINE]), + new HttpHeaders(headerLines), + new RequestBody(requestBody) + ); + } +} diff --git a/mvc/src/main/java/project/server/mvc/tomcat/Nio2EndPoint.java b/mvc/src/main/java/project/server/mvc/tomcat/Nio2EndPoint.java new file mode 100644 index 0000000..404f532 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/tomcat/Nio2EndPoint.java @@ -0,0 +1,89 @@ +package project.server.mvc.tomcat; + +import java.util.Objects; +import java.util.Queue; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.function.Predicate; + +public class Nio2EndPoint extends AbstractEndpoint { + + private final Poller poller; + + public Nio2EndPoint(ExecutorService executorService) { + super(executorService); + this.poller = new Poller(); + } + + class Poller implements Runnable { + + private static final Queue events = new ConcurrentLinkedQueue<>(); + + @Override + public void run() { + + } + + public void register(NioSocketWrapper socketWrapper) { + PollerEvent event = createPollerEvent(socketWrapper); + addEvent(event); + } + + private PollerEvent createPollerEvent(NioSocketWrapper socketWrapper) { + return new PollerEvent(socketWrapper); + } + + private void addEvent(PollerEvent event) { + events.offer(event); + } + + public void events(UUID uuid) { + PollerEvent findEvent = events.stream() + .filter(equals(uuid)) + .findAny() + .orElseGet(() -> null); + events.remove(findEvent); + } + + private Predicate equals(UUID uuid) { + return event -> event.uuid.equals(uuid); + } + } + + public static class PollerEvent { + + private final UUID uuid; + private NioSocketWrapper socketWrapper; + + public PollerEvent(NioSocketWrapper socketWrapper) { + this.uuid = UUID.randomUUID(); + this.socketWrapper = socketWrapper; + } + + public UUID getUuid() { + return uuid; + } + + public NioSocketWrapper getSocketWrapper() { + return socketWrapper; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + PollerEvent event = (PollerEvent) object; + return uuid.equals(event.uuid); + } + + @Override + public int hashCode() { + return Objects.hash(uuid); + } + } +} diff --git a/mvc/src/main/java/project/server/mvc/tomcat/NioSocketWrapper.java b/mvc/src/main/java/project/server/mvc/tomcat/NioSocketWrapper.java new file mode 100644 index 0000000..1f4b284 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/tomcat/NioSocketWrapper.java @@ -0,0 +1,4 @@ +package project.server.mvc.tomcat; + +public class NioSocketWrapper { +} diff --git a/mvc/src/main/java/project/server/mvc/tomcat/PortFinder.java b/mvc/src/main/java/project/server/mvc/tomcat/PortFinder.java new file mode 100644 index 0000000..0f779e3 --- /dev/null +++ b/mvc/src/main/java/project/server/mvc/tomcat/PortFinder.java @@ -0,0 +1,31 @@ +package project.server.mvc.tomcat; + +public final class PortFinder { + + private PortFinder() { + throw new AssertionError("์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ์‹์œผ๋กœ ์ƒ์„ฑ์ž๋ฅผ ํ˜ธ์ถœํ•ด์ฃผ์„ธ์š”."); + } + + private static final int MIN_PORT_NUMBER = 1; + private static final int MAX_PORT_NUMBER = 65535; + private static final int DEFAULT_PORT = 8086; + + public static int findPort(String[] args) { + if (args == null || args.length == 0) { + return DEFAULT_PORT; + } + return parsePort(args); + } + + private static int parsePort(String[] args) { + try { + int port = Integer.parseInt(args[0]); + if (port < MIN_PORT_NUMBER || port > MAX_PORT_NUMBER) { + return DEFAULT_PORT; + } + return port; + } catch (NumberFormatException exception) { + return DEFAULT_PORT; + } + } +} diff --git a/mvc/src/test/java/project/server/mvc/common/fixture/HeaderFixture.java b/mvc/src/test/java/project/server/mvc/common/fixture/HeaderFixture.java index 420afd0..e33ecc8 100644 --- a/mvc/src/test/java/project/server/mvc/common/fixture/HeaderFixture.java +++ b/mvc/src/test/java/project/server/mvc/common/fixture/HeaderFixture.java @@ -6,10 +6,10 @@ private HeaderFixture() { throw new AssertionError("์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ์‹์œผ๋กœ ์ƒ์„ฑ์ž๋ฅผ ํ˜ธ์ถœํ•ด์ฃผ์„ธ์š”."); } - public static final String HTTP_REQUEST = "Host: www.google.com\n" + - "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\n" + - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\n" + - "Accept-Language: en-US,en;q=0.5\n" + - "Accept-Encoding: gzip, deflate, br\n" + - "Connection: keep-alive\n"; + public static final String HTTP_REQUEST = "Host: www.google.com\n" + + "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\n" + + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\n" + + "Accept-Language: en-US,en;q=0.5\n" + + "Accept-Encoding: gzip, deflate, br\n" + + "Connection: keep-alive\n"; } diff --git a/mvc/src/test/java/project/server/mvc/test/unittest/http/CookiesUnitTest.java b/mvc/src/test/java/project/server/mvc/test/unittest/http/CookiesUnitTest.java index 933898a..f504929 100644 --- a/mvc/src/test/java/project/server/mvc/test/unittest/http/CookiesUnitTest.java +++ b/mvc/src/test/java/project/server/mvc/test/unittest/http/CookiesUnitTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import project.server.mvc.servlet.http.Cookie; import project.server.mvc.servlet.http.Cookies; @DisplayName("[UnitTest] ์ฟ ํ‚ค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ") @@ -21,6 +22,8 @@ void cookiesInitTest() { @DisplayName("์ฟ ํ‚ค ๋ชฉ๋ก์— key๋ฅผ ๋„ฃ์œผ๋ฉด value ๊ฐ’์ด ๋‚˜์˜จ๋‹ค.") void cookieGetValueTest() { Cookies cookies = new Cookies(List.of("name=value")); - assertEquals("value", cookies.getValue("name")); + Cookie cookie = cookies.getValue("name"); + + assertEquals("value", cookie.value()); } } diff --git a/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestBodyUnitTest.java b/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestBodyUnitTest.java index 923e372..57ec3ad 100644 --- a/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestBodyUnitTest.java +++ b/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestBodyUnitTest.java @@ -19,7 +19,7 @@ void requestBodyParseUnitTest() { @Test @DisplayName("RequestBody์—์„œ Username, Password๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.") - public void requestBodyGetAttributeUnitTest() { + void requestBodyGetAttributeUnitTest() { RequestBody requestBody = new RequestBody(REQUEST_BODY); assertAll( () -> assertEquals("Steve-Jobs", requestBody.getAttribute("username")), diff --git a/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestHeaderUnitTest.java b/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestHeaderUnitTest.java index 84161aa..af6b3c1 100644 --- a/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestHeaderUnitTest.java +++ b/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestHeaderUnitTest.java @@ -53,7 +53,7 @@ private List parseHttpHeaders() { headerLines.add(headerLine); } } catch (IOException exception) { - exception.printStackTrace(); + throw new RuntimeException(); } return headerLines; } diff --git a/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestLineUnitTest.java b/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestLineUnitTest.java index e304e50..1987071 100644 --- a/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestLineUnitTest.java +++ b/mvc/src/test/java/project/server/mvc/test/unittest/http/RequestLineUnitTest.java @@ -24,7 +24,7 @@ void requestLineParseTest() throws IOException { assertAll( () -> assertEquals(GET, requestLine.getHttpMethod()), - () -> assertEquals("/index.html", requestLine.getRequestUrl()), + () -> assertEquals("/index.html", requestLine.getRequestUri()), () -> assertEquals("text/html", requestLine.getContentType()), () -> assertEquals(HTTP_1_1.getValue(), "HTTP/1.1") );