diff --git a/src/main/java/app/db/ArticleRepository.java b/src/main/java/app/db/ArticleRepository.java new file mode 100644 index 000000000..86e09ac29 --- /dev/null +++ b/src/main/java/app/db/ArticleRepository.java @@ -0,0 +1,11 @@ +package app.db; + +import app.model.Article; +import database.ConnectionManager; +import database.CrudRepository; + +public class ArticleRepository extends CrudRepository
{ + public ArticleRepository(ConnectionManager connectionManager) { + super(connectionManager, Article.class); + } +} diff --git a/src/main/java/app/db/CommentRepository.java b/src/main/java/app/db/CommentRepository.java new file mode 100644 index 000000000..e1f6583f4 --- /dev/null +++ b/src/main/java/app/db/CommentRepository.java @@ -0,0 +1,11 @@ +package app.db; + +import app.model.Comment; +import database.ConnectionManager; +import database.CrudRepository; + +public class CommentRepository extends CrudRepository { + public CommentRepository(ConnectionManager connectionManager) { + super(connectionManager, Comment.class); + } +} diff --git a/src/main/java/app/handler/ArticleLikeIncreaseHandler.java b/src/main/java/app/handler/ArticleLikeIncreaseHandler.java new file mode 100644 index 000000000..9f0339667 --- /dev/null +++ b/src/main/java/app/handler/ArticleLikeIncreaseHandler.java @@ -0,0 +1,29 @@ +package app.handler; + +import app.db.ArticleRepository; +import app.model.Article; +import exception.ErrorCode; +import exception.ServiceException; +import http.HttpMethod; +import web.dispatch.argument.QueryParameters; +import web.handler.SingleArgHandler; +import web.response.HandlerResponse; +import web.response.RedirectResponse; + +public class ArticleLikeIncreaseHandler extends SingleArgHandler { + private final ArticleRepository articleRepository; + public ArticleLikeIncreaseHandler(ArticleRepository articleRepository) { + super(HttpMethod.POST, "/like"); + this.articleRepository = articleRepository; + } + + @Override + public HandlerResponse handle(QueryParameters params) { + Article article = articleRepository.findById( + Long.parseLong(params.getValidQueryValue("articleId"))).orElseThrow( + () -> new ServiceException(ErrorCode.NO_SUCH_RESOURCE)); + article.increaseLikeCount(); + articleRepository.update(article); + return RedirectResponse.to("/?articleId=" + article.getId()); + } +} diff --git a/src/main/java/app/handler/CreateArticleWithPost.java b/src/main/java/app/handler/CreateArticleWithPost.java new file mode 100644 index 000000000..fe890c276 --- /dev/null +++ b/src/main/java/app/handler/CreateArticleWithPost.java @@ -0,0 +1,79 @@ +package app.handler; + +import app.db.ArticleRepository; +import app.model.Article; +import config.DatabaseConfig; +import exception.ErrorCode; +import exception.ErrorException; +import exception.ServiceException; +import http.HttpMethod; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import web.dispatch.argument.MultipartFile; +import web.dispatch.argument.MultipartForm; +import web.filter.authentication.AuthenticationInfo; +import web.handler.DoubleArgHandler; +import web.response.HandlerResponse; +import web.response.RedirectResponse; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +import static config.DatabaseConfig.ARTICLE_IMG_DIR; +import static exception.ErrorCode.UNSUPPORTED_IMAGE_TYPE; + +public class CreateArticleWithPost extends DoubleArgHandler { + private static final Logger log = LoggerFactory.getLogger(CreateArticleWithPost.class); + private final ArticleRepository articleRepository; + + public CreateArticleWithPost(ArticleRepository articleRepository) { + super(HttpMethod.POST, "/article/create"); + this.articleRepository = articleRepository; + } + + @Override + public HandlerResponse handle(MultipartForm multiform, AuthenticationInfo authInfo) { + MultipartFile multipartFile = multiform.getFile("file").orElseThrow( + () -> new ErrorException("No file in multiform")); + Long userId = authInfo.getUserId().orElseThrow( + () -> new ServiceException(ErrorCode.UNAUTHORIZED)); + + String extension = extractExtension(multipartFile); + + Article saved = articleRepository.save( + new Article( + authInfo.getUserId().get(), + multiform.getField("content") + .orElse(""))); + + Path filePath = Paths.get(ARTICLE_IMG_DIR) + .resolve(saved.getId().toString()); + + try { + Files.write(filePath, + multipartFile.bytes(), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException e){ + log.error(e.fillInStackTrace().toString()); + throw new ErrorException("error"); + } + log.info("Article id[{}] created by {}({})", + saved.getId(), + userId, + authInfo.getAttribute("nickname")); + return RedirectResponse.to("/"); + } + + private static String extractExtension(MultipartFile multipartFile) { + return switch(multipartFile.contentType()){ + case "image/png" -> ".png"; + case "image/jpg" -> ".jpg"; + case "image/jpeg" -> ".jpeg"; + default -> throw new ServiceException(UNSUPPORTED_IMAGE_TYPE); + }; + } +} diff --git a/src/main/java/app/handler/CreateCommentWithPost.java b/src/main/java/app/handler/CreateCommentWithPost.java new file mode 100644 index 000000000..f41ebc493 --- /dev/null +++ b/src/main/java/app/handler/CreateCommentWithPost.java @@ -0,0 +1,38 @@ +package app.handler; + +import app.db.CommentRepository; +import app.model.Comment; +import exception.ErrorCode; +import exception.ServiceException; +import http.HttpMethod; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import web.dispatch.argument.QueryParameters; +import web.filter.authentication.AuthenticationInfo; +import web.handler.DoubleArgHandler; +import web.response.HandlerResponse; +import web.response.RedirectResponse; + +public class CreateCommentWithPost extends DoubleArgHandler { + private static final Logger log = LoggerFactory.getLogger(CreateCommentWithPost.class); + private final CommentRepository commentRepository; + public CreateCommentWithPost(CommentRepository commentRepository) { + super(HttpMethod.POST, "/comment/create"); + this.commentRepository = commentRepository; + } + + @Override + public HandlerResponse handle(QueryParameters params, AuthenticationInfo authInfo) { + String content = params.getValidQueryValue("content"); + Long articleId = Long.parseLong( + params.getValidQueryValue("articleId")); + Long userId = authInfo.getUserId() + .orElseThrow( + () -> new ServiceException(ErrorCode.UNAUTHORIZED)); + commentRepository.save(new Comment(articleId, userId, content)); + + log.info("{}->{} - comment created", userId, articleId); + + return RedirectResponse.to("/?articleId=" + articleId); + } +} diff --git a/src/main/java/app/handler/GetCommentCreateForm.java b/src/main/java/app/handler/GetCommentCreateForm.java new file mode 100644 index 000000000..47259bd55 --- /dev/null +++ b/src/main/java/app/handler/GetCommentCreateForm.java @@ -0,0 +1,21 @@ +package app.handler; + +import http.HttpMethod; +import http.HttpStatus; +import web.dispatch.argument.QueryParameters; +import web.handler.SingleArgHandler; +import web.response.DynamicViewResponse; +import web.response.HandlerResponse; + +public class GetCommentCreateForm extends SingleArgHandler { + public GetCommentCreateForm() { + super(HttpMethod.GET, "/comment"); + } + + @Override + public HandlerResponse handle(QueryParameters arg) { + DynamicViewResponse response = DynamicViewResponse.of(HttpStatus.OK, "/comment/index.html"); + response.addModel("articleId", arg.getValidQueryValue("articleId")); + return response; + } +} diff --git a/src/main/java/app/handler/HomeHandler.java b/src/main/java/app/handler/HomeHandler.java index 480eeb75a..d4fbc6748 100644 --- a/src/main/java/app/handler/HomeHandler.java +++ b/src/main/java/app/handler/HomeHandler.java @@ -1,20 +1,96 @@ package app.handler; +import app.db.ArticleRepository; +import app.db.CommentRepository; +import app.db.UserRepository; +import app.model.Article; +import app.model.Comment; +import app.model.User; +import exception.ErrorCode; +import exception.ErrorException; +import exception.ServiceException; import http.HttpMethod; import http.HttpStatus; -import http.request.HttpRequest; +import web.dispatch.argument.QueryParameters; import web.handler.SingleArgHandler; import web.response.DynamicViewResponse; import web.response.HandlerResponse; -public class HomeHandler extends SingleArgHandler { +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; - public HomeHandler() { +public class HomeHandler extends SingleArgHandler { + private final ArticleRepository articleRepository; + private final UserRepository userRepository; + private final CommentRepository commentRepository; + + public HomeHandler(ArticleRepository articleRepository, UserRepository userRepository, CommentRepository commentRepository) { super(HttpMethod.GET, "/"); + this.articleRepository = articleRepository; + this.userRepository = userRepository; + this.commentRepository = commentRepository; } @Override - public HandlerResponse handle(HttpRequest request) { - return DynamicViewResponse.of(HttpStatus.OK, "/index.html"); + public HandlerResponse handle(QueryParameters params) { + Article article; + List
articleList = articleRepository.findAll(); + DynamicViewResponse response = + DynamicViewResponse.of(HttpStatus.OK, "/index.html"); + if (params.getQueryValue("articleId").isPresent()) { + article = articleRepository.findById( + Long.parseLong(params.getQueryValue("articleId").get())) + .orElseThrow( + () -> new ServiceException(ErrorCode.NO_SUCH_RESOURCE)); + } else { + if (articleList.isEmpty()) { + response.addModel("article", null); + return response; + } + article = articleList.get(articleList.size() - 1); + } + User writer = userRepository.findById(article.getWriterId()) + .orElseThrow( + ()-> new ErrorException("Writer not exists")); + + List commentList = commentRepository.findByColumn("articleId", article.getId()); + response.addModel("commentCount", commentList.size()); + if (params.getQueryValue("fullComments").isEmpty() + || params.getQueryValue("fullComments").get().equals("false")) { + if(commentList.size()>3) { + response.addModel("hasMoreComments", true); + response.addModel("additionalComments", commentList.size()-3); + commentList = commentList.subList(0, 3); + } else{ + response.addModel("hasMoreComments", false); + } + } + + response.addModel("article", article); + response.addModel("writerNickname", writer.getNickname()); + response.addModel("commentList", toCommentDtoList(commentList)); + response.addModel("next", article.getId() < articleList.size() + ? article.getId() + 1 : false); + response.addModel("prev", article.getId() > 1 + ? article.getId()-1 : false); + + return response; + } + + private List> toCommentDtoList(List commentList){ + List> commentDtoList = new ArrayList<>(); + for (Comment comment : commentList) { + Map commentDto = new HashMap<>(); + commentDto.put("writerId", comment.getWriterId()); + commentDto.put("content", comment.getContent()); + String nickname = userRepository.findById(comment.getWriterId()).orElseThrow( + () -> new ErrorException("Comment Writer Id Not Exists")) + .getNickname(); + commentDto.put("nickname", nickname); + commentDtoList.add(commentDto); + } + return commentDtoList; } } diff --git a/src/main/java/app/handler/LoginWithPost.java b/src/main/java/app/handler/LoginWithPost.java index 987ad8160..b455fd645 100644 --- a/src/main/java/app/handler/LoginWithPost.java +++ b/src/main/java/app/handler/LoginWithPost.java @@ -1,6 +1,7 @@ package app.handler; import app.db.Database; +import app.db.UserRepository; import app.model.User; import config.VariableConfig; import exception.ErrorCode; @@ -14,25 +15,32 @@ import web.session.SessionEntity; import web.session.SessionStorage; +import java.util.List; + public class LoginWithPost extends SingleArgHandler { + private static final String EMAIL = "email"; + private static final String PASSWORD = "password"; + + private final SessionStorage sessionManager; + private final UserRepository userRepository; - public LoginWithPost(SessionStorage sessionManager) { + public LoginWithPost(SessionStorage sessionManager, UserRepository userRepository) { super(HttpMethod.POST, "/user/login"); this.sessionManager = sessionManager; + this.userRepository = userRepository; } @Override public HandlerResponse handle(QueryParameters params) { - String email = params.getQueryValue("email") - .orElseThrow(() -> new ServiceException(ErrorCode.LOGIN_FAILED, "email required")); + String email = getRequired(params, EMAIL); + String password = getRequired(params, PASSWORD); - String password = params.getQueryValue("password") - .orElseThrow(() -> new ServiceException(ErrorCode.LOGIN_FAILED, "password required")); - - User user = Database.findUserByEmail(email) - .orElseThrow(() -> new ServiceException(ErrorCode.LOGIN_FAILED)); + List userList = userRepository.findByColumn(EMAIL, email); + if(userList.isEmpty()) + throw new ServiceException(ErrorCode.EMAIL_NOT_FOUND, "회원가입을 하시겠습니까?"); + User user = userList.get(0); if (!user.getPassword().equals(password)) { throw new ServiceException(ErrorCode.LOGIN_FAILED); } @@ -52,4 +60,9 @@ public HandlerResponse handle(QueryParameters params) { ); return response; } + + private String getRequired(QueryParameters params, String key) { + return params.getQueryValue(key) + .orElseThrow(() -> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, key + " required")); + } } diff --git a/src/main/java/app/handler/MypageHandler.java b/src/main/java/app/handler/MypageHandler.java new file mode 100644 index 000000000..3156c9429 --- /dev/null +++ b/src/main/java/app/handler/MypageHandler.java @@ -0,0 +1,37 @@ +package app.handler; + +import exception.ErrorException; +import http.HttpMethod; +import http.HttpStatus; +import web.filter.authentication.AuthenticationInfo; +import web.handler.SingleArgHandler; +import web.response.DynamicViewResponse; +import web.response.HandlerResponse; + +import java.io.File; + +import static config.DatabaseConfig.BASIC_PROFILE_IMG; +import static config.DatabaseConfig.PROFILE_IMG_DIR; + +public class MypageHandler extends SingleArgHandler { + public MypageHandler() { + super(HttpMethod.GET, "/mypage"); + } + + @Override + public HandlerResponse handle(AuthenticationInfo info) { + DynamicViewResponse response = DynamicViewResponse.of(HttpStatus.OK, "/mypage/index.html"); + Long userId = info.getUserId().orElseThrow( + () -> new ErrorException("MypageHandler::User id must exists")); + + File requestedFile = new File(PROFILE_IMG_DIR + "/" + userId); + if (requestedFile.exists() && requestedFile.isFile()){ + response.addModel("profileImageUrl", "/mypage/img/" + userId); + } else{ + response.addModel("profileImageUrl", BASIC_PROFILE_IMG); + } + response.addModel("defaultProfileImageUrl", BASIC_PROFILE_IMG); + + return response; + } +} diff --git a/src/main/java/app/handler/MypageUpdateHandler.java b/src/main/java/app/handler/MypageUpdateHandler.java new file mode 100644 index 000000000..9ce8f984a --- /dev/null +++ b/src/main/java/app/handler/MypageUpdateHandler.java @@ -0,0 +1,119 @@ +package app.handler; + +import app.db.UserRepository; +import app.model.User; +import config.DatabaseConfig; +import exception.ErrorCode; +import exception.ErrorException; +import exception.ServiceException; +import http.HttpMethod; +import http.request.HttpRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import web.dispatch.argument.MultipartFile; +import web.dispatch.argument.MultipartForm; +import web.filter.authentication.AuthenticationInfo; +import web.handler.DoubleArgHandler; +import web.response.HandlerResponse; +import web.response.RedirectResponse; +import web.session.SessionEntity; +import web.session.SessionStorage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +import static config.DatabaseConfig.PROFILE_IMG_DIR; +import static config.VariableConfig.*; +import static exception.ErrorCode.*; + +public class MypageUpdateHandler extends DoubleArgHandler { + private static final Logger log = LoggerFactory.getLogger(MypageUpdateHandler.class); + private final UserRepository userRepository; + private final SessionStorage sessionStorage; + + public MypageUpdateHandler(UserRepository userRepository, SessionStorage sessionStorage) { + super(HttpMethod.POST, "/mypage/update"); + this.userRepository = userRepository; + this.sessionStorage = sessionStorage; + } + + @Override + public HandlerResponse handle(MultipartForm form, HttpRequest request) { + AuthenticationInfo info = request.getAuthenticationInfo(); + Long userId = info.getUserId().orElseThrow( + () -> new ErrorException("user id should exists at /mypage")); + MultipartFile file = form.getFile("profileImage") + .orElseThrow( + ()-> new ServiceException(INVALID_INPUT, "사진이 포함되어야 합니다.")); + if(file!=null && file.bytes().length>0){ + Path filePath = Paths.get(PROFILE_IMG_DIR) + .resolve(userId.toString()); + try { + log.debug("write file to {}", filePath); + Files.write(filePath, + file.bytes(), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException e){ + log.error(e.fillInStackTrace().toString()); + throw new ErrorException("error"); + } + } else{ + try { + Files.deleteIfExists(Paths.get(PROFILE_IMG_DIR) + .resolve(userId.toString())); + } catch (IOException e) { + log.error(e.getStackTrace().toString()); + } + } + + User user = userRepository.findById(userId) + .orElseThrow( + () -> new ErrorException("MypageUpdateHandlerError")); + + if(form.getField("nickname").isPresent() + && !form.getField("nickname").get().isBlank()){ + String nickname = form.getField("nickname").get(); + validateLength(nickname, NICKNAME_MIN, NICKNAME_MAX, NICKNAME_LENGTH_INVALID); + user.setNickname(nickname); + SessionEntity sessionEntity = sessionStorage.getValid( + request.getCookieValue("SID") + .orElseThrow( + () -> new ErrorException("mypage update error"))); + sessionEntity.setNickname(nickname); + } + if (form.getField("password").isPresent() + && form.getField("passwordConfirm").isPresent() + && !form.getField("password").get().isBlank()) { + String password = form.getField("password").get(); + validateLength(password, PASSWORD_MIN, PASSWORD_MAX, PASSWORD_LENGTH_INVALID); + if (password.equals(form.getField("passwordConfirm").get())) { + user.setPassword(password); + } else { + throw new ServiceException(PASSWORD_DOUBLE_CHECK_FAIL); + } + } + + userRepository.update(user); + + return RedirectResponse.to("/mypage"); + } + + private void validateLength(String value, int min, int max, ErrorCode code) { + int len = value.length(); + if (len < min || len > max) + throw new ServiceException(code); + } + + private static String extractExtension(MultipartFile multipartFile) { + return switch(multipartFile.contentType().toLowerCase()){ + case "image/png" -> ".png"; + case "image/jpg" -> ".jpg"; + case "image/jpeg" -> ".jpeg"; + default -> throw new ServiceException(UNSUPPORTED_IMAGE_TYPE); + }; + } +} diff --git a/src/main/java/app/handler/RegisterWithPost.java b/src/main/java/app/handler/RegisterWithPost.java index 410dc497d..e2e0433bf 100644 --- a/src/main/java/app/handler/RegisterWithPost.java +++ b/src/main/java/app/handler/RegisterWithPost.java @@ -14,6 +14,9 @@ import web.response.HandlerResponse; import web.response.RedirectResponse; +import static config.VariableConfig.*; +import static exception.ErrorCode.*; + public class RegisterWithPost extends SingleArgHandler { private static final String EMAIL = "email"; private static final String NICKNAME = "nickname"; @@ -46,14 +49,14 @@ public HandlerResponse handle(QueryParameters params) { private void validate(String email, String nickname, String password) { validateDuplicate(email, nickname); - validateLength(email, VariableConfig.EMAIL_MIN, VariableConfig.EMAIL_MAX, ErrorCode.EMAIL_LENGTH_INVALID); - validateLength(nickname, VariableConfig.NICKNAME_MIN, VariableConfig.NICKNAME_MAX, ErrorCode.NICKNAME_LENGTH_INVALID); - validateLength(password, VariableConfig.PASSWORD_MIN, VariableConfig.PASSWORD_MAX, ErrorCode.PASSWORD_LENGTH_INVALID); + validateLength(email, EMAIL_MIN, EMAIL_MAX, EMAIL_LENGTH_INVALID); + validateLength(nickname, NICKNAME_MIN, NICKNAME_MAX, NICKNAME_LENGTH_INVALID); + validateLength(password, PASSWORD_MIN, PASSWORD_MAX, PASSWORD_LENGTH_INVALID); } private String getRequired(QueryParameters params, String key) { return params.getQueryValue(key) - .orElseThrow(() -> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, key + " required")); + .orElseThrow(() -> new ServiceException(MISSING_REGISTER_TOKEN, key + " required")); } private void validateLength(String value, int min, int max, ErrorCode code) { @@ -64,9 +67,9 @@ private void validateLength(String value, int min, int max, ErrorCode code) { private void validateDuplicate(String email, String nickname) { if (!userRepository.findByColumn(EMAIL, email).isEmpty()) - throw new ServiceException(ErrorCode.EMAIL_ALREADY_EXISTS); + throw new ServiceException(EMAIL_ALREADY_EXISTS); if (!userRepository.findByColumn(NICKNAME, nickname).isEmpty()) - throw new ServiceException(ErrorCode.NICKNAME_ALREADY_EXISTS); + throw new ServiceException(NICKNAME_ALREADY_EXISTS); } } diff --git a/src/main/java/app/model/Article.java b/src/main/java/app/model/Article.java new file mode 100644 index 000000000..2fc0a2547 --- /dev/null +++ b/src/main/java/app/model/Article.java @@ -0,0 +1,44 @@ +package app.model; + +import java.time.LocalDateTime; + +public class Article { + private Long id; + private Long likeCount; + private Long writerId; + private String content; + private LocalDateTime createdAt; + + public Article(Long writerId, String content) { + this.likeCount = 0L; + this.writerId = writerId; + this.content = content; + this.createdAt = LocalDateTime.now(); + } + + public Article() {} + + public Long getId() { + return id; + } + + public Long getLikeCount() { + return likeCount; + } + + public Long getWriterId() { + return writerId; + } + + public String getContent() { + return content; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public Long increaseLikeCount(){ + return ++this.likeCount; + } +} diff --git a/src/main/java/app/model/Comment.java b/src/main/java/app/model/Comment.java new file mode 100644 index 000000000..53df4a6f1 --- /dev/null +++ b/src/main/java/app/model/Comment.java @@ -0,0 +1,39 @@ +package app.model; + +import java.time.LocalDateTime; + +public class Comment { + private Long id; + private Long articleId; + private Long writerId; + private String content; + private LocalDateTime createdAt; + + public Comment(Long articleId, Long writerId, String content) { + this.articleId = articleId; + this.writerId = writerId; + this.content = content; + } + + public Comment(){} + + public Long getId() { + return id; + } + + public Long getArticleId() { + return articleId; + } + + public Long getWriterId() { + return writerId; + } + + public String getContent() { + return content; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } +} diff --git a/src/main/java/app/model/User.java b/src/main/java/app/model/User.java index 7a1dd54bc..e24f813cc 100644 --- a/src/main/java/app/model/User.java +++ b/src/main/java/app/model/User.java @@ -29,10 +29,18 @@ public String getPassword() { return password; } + public void setPassword(String password) { + this.password = password; + } + public String getNickname() { return nickname; } + public void setNickname(String nickname) { + this.nickname = nickname; + } + public String getEmail() { return email; } @@ -40,9 +48,4 @@ public String getEmail() { public String getUserRole() { return userRole; } - - @Override - public String toString() { - return "User [userId=" + id + ", password=" + password + ", name=" + nickname + ", email=" + email + "]"; - } } diff --git a/src/main/java/config/AppConfig.java b/src/main/java/config/AppConfig.java index 3fb391624..0aa499b2a 100644 --- a/src/main/java/config/AppConfig.java +++ b/src/main/java/config/AppConfig.java @@ -1,5 +1,7 @@ package config; +import app.db.ArticleRepository; +import app.db.CommentRepository; import app.db.UserRepository; import app.handler.*; import database.ConnectionManager; @@ -16,15 +18,14 @@ import web.dispatch.Dispatcher; import web.dispatch.HandlerAdapter; import web.dispatch.adapter.DefaultHandlerAdapter; +import web.dispatch.adapter.DoubleArgHandlerAdapter; import web.dispatch.adapter.SingleArgHandlerAdapter; import web.dispatch.argument.ArgumentResolver; -import web.dispatch.argument.resolver.HttpRequestResolver; -import web.dispatch.argument.resolver.MultipartFormParser; -import web.dispatch.argument.resolver.MultipartFormResolver; -import web.dispatch.argument.resolver.QueryParamsResolver; +import web.dispatch.argument.resolver.*; import web.filter.*; import web.handler.DefaultViewHandler; import web.handler.StaticContentHandler; +import web.handler.UserProfileImageHandler; import web.handler.WebHandler; import web.renderer.DynamicViewRenderer; import web.renderer.HttpResponseRenderer; @@ -94,6 +95,13 @@ public List webHandlerList() { registerWithPost(), loginWithPost(), logoutWithPost(), + createArticleWithPost(), + createCommentWithPost(), + getCommentCreateForm(), + articleLikeIncreaseHandler(), + mypageHandler(), + mypageUpdateHandler(), + userProfileImageHandler(), homeHandler(), defaultViewHandler()) ); @@ -126,7 +134,9 @@ public RegisterWithPost registerWithPost() { public LoginWithPost loginWithPost() { return getOrCreate("loginWithPost", - () -> new LoginWithPost(sessionStorage())); + () -> new LoginWithPost( + sessionStorage(), + userRepository())); } public LogoutWithPost logoutWithPost(){ @@ -134,8 +144,59 @@ public LogoutWithPost logoutWithPost(){ () -> new LogoutWithPost(sessionStorage())); } + public CreateArticleWithPost createArticleWithPost(){ + return getOrCreate( + CreateArticleWithPost.class.getSimpleName(), + () -> new CreateArticleWithPost( + articleRepository())); + } + + public CreateCommentWithPost createCommentWithPost(){ + return getOrCreate( + CreateCommentWithPost.class.getSimpleName(), + () -> new CreateCommentWithPost( + commentRepository())); + } + + public GetCommentCreateForm getCommentCreateForm(){ + return getOrCreate( + GetCommentCreateForm.class.getSimpleName(), + GetCommentCreateForm::new); + } + + public ArticleLikeIncreaseHandler articleLikeIncreaseHandler(){ + return getOrCreate( + ArticleLikeIncreaseHandler.class.getSimpleName(), + () -> new ArticleLikeIncreaseHandler( + articleRepository())); + } + + public MypageHandler mypageHandler(){ + return getOrCreate( + MypageHandler.class.getSimpleName(), + MypageHandler::new); + } + + public MypageUpdateHandler mypageUpdateHandler(){ + return getOrCreate( + MypageUpdateHandler.class.getSimpleName(), + () -> new MypageUpdateHandler( + userRepository(), + sessionStorage())); + } + + public UserProfileImageHandler userProfileImageHandler(){ + return getOrCreate( + UserProfileImageHandler.class.getSimpleName(), + UserProfileImageHandler::new); + } + public HomeHandler homeHandler(){ - return getOrCreate("homeHandler", HomeHandler::new); + return getOrCreate("homeHandler", + () -> new HomeHandler( + articleRepository(), + userRepository(), + commentRepository())); } // ===== Renderer ===== @@ -186,6 +247,7 @@ public List handlerAdapterList() { "handlerAdapterList", () -> List.of( singleArgHandlerAdapter(), + doubleArgHandlerAdapter(), defaultHandlerAdapter() ) ); @@ -200,6 +262,15 @@ public SingleArgHandlerAdapter singleArgHandlerAdapter() { ); } + public DoubleArgHandlerAdapter doubleArgHandlerAdapter(){ + return getOrCreate( + DoubleArgHandlerAdapter.class.getSimpleName(), + () -> new DoubleArgHandlerAdapter( + argumentResolverList() + ) + ); + } + public DefaultHandlerAdapter defaultHandlerAdapter() { return getOrCreate( "defaultHandlerAdapter", @@ -213,7 +284,8 @@ public List> argumentResolverList() { () -> List.of( httpRequestResolver(), queryParamsResolver(), - multipartFormResolver() + multipartFormResolver(), + authenticationInfoResolver() ) ); } @@ -240,6 +312,12 @@ public MultipartFormResolver multipartFormResolver(){ public MultipartFormParser multipartFormParser(){ return getOrCreate("multipartFormParser", MultipartFormParser::new); } + + public AuthenticationInfoResolver authenticationInfoResolver(){ + return getOrCreate( + AuthenticationInfoResolver.class.getSimpleName(), + AuthenticationInfoResolver::new); + } /** * ===== Exception ===== */ @@ -330,5 +408,15 @@ public UserRepository userRepository(){ return getOrCreate(UserRepository.class.getSimpleName(), ()-> new UserRepository(connectionManager())); } + + public ArticleRepository articleRepository(){ + return getOrCreate(ArticleRepository.class.getSimpleName(), + () -> new ArticleRepository(connectionManager())); + } + + public CommentRepository commentRepository(){ + return getOrCreate(CommentRepository.class.getSimpleName(), + () -> new CommentRepository(connectionManager())); + } } diff --git a/src/main/java/config/DatabaseConfig.java b/src/main/java/config/DatabaseConfig.java index 70e72a47c..fd0ddfb9a 100644 --- a/src/main/java/config/DatabaseConfig.java +++ b/src/main/java/config/DatabaseConfig.java @@ -1,13 +1,26 @@ package config; +import app.model.Article; +import app.model.Comment; import app.model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; import java.util.List; public class DatabaseConfig { + private static final Logger log = LoggerFactory.getLogger(DatabaseConfig.class); private final AppConfig appConfig = new AppConfig(); public static final List> ENTITY_CLASSES = List.of( - User.class + User.class, + Article.class, + Comment.class ); public static final List RESOLVED_WORD = List.of( @@ -18,11 +31,42 @@ public class DatabaseConfig { public static final String H2_DB_USER = "sa"; public static final String H2_DB_PASSWORD = ""; + public static final String ARTICLE_IMG_DIR = "./src/main/resources/static/article/img"; + public static final String PROFILE_IMG_DIR = "./src/main/resources/static/mypage/img"; + public static final String BASIC_PROFILE_IMG = "/img/basic_profileImage.svg"; + public static final boolean CREATE_TABLES = false; public static final boolean DROP_IF_EXISTS = false; public void config(){ DdlGenerator ddlGenerator = appConfig.ddlGenerator(); - if(CREATE_TABLES) ddlGenerator.generateTables(); + if(CREATE_TABLES) { + ddlGenerator.generateTables(); + } + if(DROP_IF_EXISTS){ + resetDir(ARTICLE_IMG_DIR); + resetDir(PROFILE_IMG_DIR); + } + } + + public void resetDir(String dirPath){ + Path dir = Paths.get(dirPath); + + try { + if (Files.exists(dir)) { + Files.walk(dir) + .sorted(Comparator.reverseOrder()) // 자식부터 삭제 + .forEach(p -> { + try { + Files.deleteIfExists(p); + } catch (IOException e) { + log.error(e.fillInStackTrace().toString()); + } + }); + } + Files.createDirectories(dir); + } catch (IOException e){ + log.error(e.fillInStackTrace().toString()); + } } -} +} \ No newline at end of file diff --git a/src/main/java/config/DdlGenerator.java b/src/main/java/config/DdlGenerator.java index 0d06d7d7b..8eef08e6f 100644 --- a/src/main/java/config/DdlGenerator.java +++ b/src/main/java/config/DdlGenerator.java @@ -31,11 +31,15 @@ public void generateTables() { String tableName = toTableName(entityClass); String ddl = buildDdlForEntity(entityClass, tableName); log.info("DDL for {}:\n{}", tableName, ddl); - stmt.execute(ddl); + try { + stmt.execute(ddl); + } catch (SQLException e) { + e.printStackTrace(); + } } } catch (SQLException e) { - throw new RuntimeException("DDL 생성/실행 중 오류", e); + e.printStackTrace(); } } diff --git a/src/main/java/config/SecurityConfig.java b/src/main/java/config/SecurityConfig.java index 2c6ccf029..f95007c73 100644 --- a/src/main/java/config/SecurityConfig.java +++ b/src/main/java/config/SecurityConfig.java @@ -16,7 +16,7 @@ public void config(){ public void setPaths(){ appConfig.filterChainContainer() - .addPath(FilterType.AUTHENTICATED, "/mypage/**") + .addPaths(FilterType.AUTHENTICATED, List.of("/mypage/**", "/article/**", "/comment/**")) .addPaths(FilterType.LOG_IN, List.of("/user/login", "/login")) .addPaths(FilterType.PUBLIC, List.of("/", "/home/*")) .addPath(FilterType.ALL, "/**"); diff --git a/src/main/java/exception/ErrorCode.java b/src/main/java/exception/ErrorCode.java index 56f2d4fec..9631bb783 100644 --- a/src/main/java/exception/ErrorCode.java +++ b/src/main/java/exception/ErrorCode.java @@ -12,8 +12,12 @@ public enum ErrorCode { EMAIL_LENGTH_INVALID(HttpStatus.BAD_REQUEST, "400_EMAIL_LENGTH_INVALID", "이메일은 4 ~ 50글자 사이여야합니다."), NICKNAME_LENGTH_INVALID(HttpStatus.BAD_REQUEST, "400_NICKNAME_LENGTH_INVALID", "닉네임은 4 ~ 12글자 사이여야합니다."), PASSWORD_LENGTH_INVALID(HttpStatus.BAD_REQUEST, "400_PASSWORD_LENGTH_INVALID", "비밀번호는 4 ~ 16글자 사이여야합니다."), + UNSUPPORTED_IMAGE_TYPE(HttpStatus.BAD_REQUEST, "400_UNSUPPORTED_IMAGE_TYPE", "지원되지 않는 이미지 타입입니다."), + EMPTY_IMG_ARTICLE(HttpStatus.BAD_REQUEST, "400_EMPTY_IMG_ARTICLE", "이미지가 존재하지 않는 게시글입니다."), + EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "404_EMAIL_NOT_FOUND", "존재하지 않는 이메일입니다."), + EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "409_EMAIL_ALREADY_EXISTS", "이미 가입된 Email입니다."), NICKNAME_ALREADY_EXISTS(HttpStatus.CONFLICT, "409_NICKNAME_ALREADY_EXISTS", "이미 사용중인 닉네임입니다."), @@ -24,6 +28,9 @@ public enum ErrorCode { INVALID_INPUT(HttpStatus.BAD_REQUEST, "400_INVALID_INPUT", "입력 값이 올바르지 않습니다."), MISSING_PARAMETER(HttpStatus.BAD_REQUEST, "400_MISSING_PARAM", "필수 파라미터가 누락되었습니다."), VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "400_VALIDATION_FAIL", "유효성 검증에 실패했습니다."), + PASSWORD_DOUBLE_CHECK_FAIL(HttpStatus.BAD_REQUEST, "400_PASSWORD_DOUBLE_CHECK_FAIL", "비밀번호가 서로 다릅니다."), + + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "401_UNAUTHORIZED", "로그인이 필요합니다."), FORBIDDEN(HttpStatus.FORBIDDEN, "403_FORBIDDEN", "권한이 없습니다."), diff --git a/src/main/java/exception/handler/UnhandledErrorHandler.java b/src/main/java/exception/handler/UnhandledErrorHandler.java index ad3a4bb00..7fce0cd8f 100644 --- a/src/main/java/exception/handler/UnhandledErrorHandler.java +++ b/src/main/java/exception/handler/UnhandledErrorHandler.java @@ -22,7 +22,9 @@ public boolean support(Throwable e) { public void handle(Throwable t, Socket connection) { String body = "{\"code\":\"500_UNHANDLED\",\"message\":\"서버 내부 오류가 발생했습니다.\"}"; byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8); - logger.debug(t.getMessage()); + logger.error(t.getMessage()); + logger.error(t.fillInStackTrace().toString()); + t.printStackTrace(); String header = "HTTP/1.1 500 Internal Server Error\r\n" + "Date: " + DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now()) + "\r\n" + diff --git a/src/main/java/web/dispatch/adapter/DoubleArgHandlerAdapter.java b/src/main/java/web/dispatch/adapter/DoubleArgHandlerAdapter.java new file mode 100644 index 000000000..6490081ce --- /dev/null +++ b/src/main/java/web/dispatch/adapter/DoubleArgHandlerAdapter.java @@ -0,0 +1,49 @@ +package web.dispatch.adapter; + +import exception.ErrorException; +import http.request.HttpRequest; +import web.dispatch.HandlerAdapter; +import web.dispatch.argument.ArgumentResolver; +import web.handler.DoubleArgHandler; +import web.handler.SingleArgHandler; +import web.handler.WebHandler; +import web.response.HandlerResponse; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; + +public class DoubleArgHandlerAdapter implements HandlerAdapter { + private final List> resolverList; + + public DoubleArgHandlerAdapter(List> resolverList) { + this.resolverList = resolverList; + } + + @Override + public boolean support(WebHandler handler) { + return handler instanceof DoubleArgHandler; + } + + @Override + @SuppressWarnings("unchecked") + public HandlerResponse handle(HttpRequest request, WebHandler handler) { + DoubleArgHandler doubleArgHandler = (DoubleArgHandler) handler; + Type argumentType1 = ((ParameterizedType) doubleArgHandler.getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + Class argumentClass1 = (Class) argumentType1; + Type argumentType2 = ((ParameterizedType) doubleArgHandler.getClass().getGenericSuperclass()).getActualTypeArguments()[1]; + Class argumentClass2 = (Class) argumentType2; + + ArgumentResolver resolver1 = resolverList.stream() + .filter(r -> r.support(argumentClass1)) + .findFirst().orElseThrow(() -> new ErrorException("HandlerAdapterError: No argument resolver1 supported.")); + ArgumentResolver resolver2 = resolverList.stream() + .filter(r -> r.support(argumentClass2)) + .findFirst().orElseThrow(() -> new ErrorException("HandlerAdapterError: No argument resolver2 supported.")); + return ((DoubleArgHandler) doubleArgHandler).handle( + resolver1.resolve(request), + resolver2.resolve(request) + ); + } + +} diff --git a/src/main/java/web/dispatch/argument/QueryParameters.java b/src/main/java/web/dispatch/argument/QueryParameters.java index bb7541f1e..d895622d6 100644 --- a/src/main/java/web/dispatch/argument/QueryParameters.java +++ b/src/main/java/web/dispatch/argument/QueryParameters.java @@ -1,5 +1,8 @@ package web.dispatch.argument; +import exception.ErrorCode; +import exception.ServiceException; + import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.*; @@ -27,6 +30,11 @@ public Optional getQueryValue(String key){ return Optional.empty(); } + public String getValidQueryValue(String key){ + return getQueryValue(key) + .orElseThrow(() -> new ServiceException(ErrorCode.MISSING_PARAMETER, key + " required")); + } + public List getQueryValues(String key){ if(params.containsKey(key)) return params.get(key); @@ -47,7 +55,7 @@ private Map> parseQueryToMap(String queryString) { for (String pair : pairs) { if (pair.isEmpty()) continue; - String[] kv = pair.split("=", 2); // value에 '=' 들어가도 OK + String[] kv = pair.split("=", 2); String rawKey = kv[0]; String rawValue = kv.length == 2 ? kv[1] : ""; diff --git a/src/main/java/web/dispatch/argument/resolver/AuthenticationInfoResolver.java b/src/main/java/web/dispatch/argument/resolver/AuthenticationInfoResolver.java new file mode 100644 index 000000000..6c73603fa --- /dev/null +++ b/src/main/java/web/dispatch/argument/resolver/AuthenticationInfoResolver.java @@ -0,0 +1,12 @@ +package web.dispatch.argument.resolver; + +import http.request.HttpRequest; +import web.dispatch.argument.ArgumentResolver; +import web.filter.authentication.AuthenticationInfo; + +public class AuthenticationInfoResolver extends ArgumentResolver { + @Override + public AuthenticationInfo resolve(HttpRequest request) { + return request.getAuthenticationInfo(); + } +} diff --git a/src/main/java/web/dispatch/argument/resolver/QueryParamsResolver.java b/src/main/java/web/dispatch/argument/resolver/QueryParamsResolver.java index 7645c2ab5..ccf24a7ef 100644 --- a/src/main/java/web/dispatch/argument/resolver/QueryParamsResolver.java +++ b/src/main/java/web/dispatch/argument/resolver/QueryParamsResolver.java @@ -9,13 +9,18 @@ public class QueryParamsResolver extends ArgumentResolver { @Override public QueryParameters resolve(HttpRequest request) { - String queryString = request.getQueryString(); - if(queryString==null) { - if (request.getContentType() != null - && request.getContentType().strip().equalsIgnoreCase("application/x-www-form-urlencoded")) { - queryString = new String(request.getBody(), StandardCharsets.UTF_8); - } else queryString = ""; + String queryString = ""; + queryString += request.getQueryString() == null + ? "" : request.getQueryString(); + System.out.println(queryString); + + if (request.getContentType() != null + && request.getContentType().strip().equalsIgnoreCase("application/x-www-form-urlencoded") + && request.getBody()!=null) { + queryString += new String(request.getBody(), StandardCharsets.UTF_8); } + System.out.println(queryString); + return QueryParameters.of(queryString); } } diff --git a/src/main/java/web/filter/AuthenticationFilter.java b/src/main/java/web/filter/AuthenticationFilter.java index 2f80dc6f6..48a967d86 100644 --- a/src/main/java/web/filter/AuthenticationFilter.java +++ b/src/main/java/web/filter/AuthenticationFilter.java @@ -1,6 +1,5 @@ package web.filter; -import http.HttpStatus; import http.request.HttpRequest; import http.response.HttpResponse; import web.filter.authentication.AuthenticationInfo; diff --git a/src/main/java/web/handler/DoubleArgHandler.java b/src/main/java/web/handler/DoubleArgHandler.java new file mode 100644 index 000000000..193ffb71a --- /dev/null +++ b/src/main/java/web/handler/DoubleArgHandler.java @@ -0,0 +1,30 @@ +package web.handler; + +import http.HttpMethod; +import web.response.HandlerResponse; + +public abstract class DoubleArgHandler implements WebHandler{ + protected final HttpMethod method; + protected final String path; + + protected DoubleArgHandler(HttpMethod method, String path) { + this.method = method; + this.path = path; + } + + public String getPath() { + return path; + } + + @Override + public HttpMethod getMethod() { + return method; + } + + @Override + public boolean checkEndpoint(HttpMethod method, String path) { + return this.method.equals(method) && this.path.equals(path); + } + + public abstract HandlerResponse handle(T arg1, V arg2); +} diff --git a/src/main/java/web/handler/UserProfileImageHandler.java b/src/main/java/web/handler/UserProfileImageHandler.java new file mode 100644 index 000000000..78579ff32 --- /dev/null +++ b/src/main/java/web/handler/UserProfileImageHandler.java @@ -0,0 +1,42 @@ +package web.handler; + +import config.DatabaseConfig; +import config.VariableConfig; +import exception.ErrorException; +import http.HttpMethod; +import http.request.HttpRequest; +import web.response.HandlerResponse; +import web.response.StaticViewResponse; + +import java.io.File; +import java.util.List; + +public class UserProfileImageHandler implements DefaultHandler { + private final HttpMethod method = HttpMethod.GET; + + @Override + public String getPath() { + throw new ErrorException("StaticContentHandler::getPath should not be called"); + } + + @Override + public HttpMethod getMethod() { + return this.method; + } + + @Override + public boolean checkEndpoint(HttpMethod method, String path) { + if(!method.equals(this.method)) return false; + if(!path.startsWith("/static/mypage/img/")) return false; + return true; + } + + @Override + public HandlerResponse handle(HttpRequest request) { + File requestedFile = new File(DatabaseConfig.PROFILE_IMG_DIR + request.getPath()); + if (requestedFile.exists() && requestedFile.isFile()){ + return StaticViewResponse.of(request.getPath()); + } + return StaticViewResponse.of("/img/basic_profileImage.svg"); + } +} diff --git a/src/main/java/web/session/SessionEntity.java b/src/main/java/web/session/SessionEntity.java index a2fd7c870..f5cace467 100644 --- a/src/main/java/web/session/SessionEntity.java +++ b/src/main/java/web/session/SessionEntity.java @@ -4,7 +4,7 @@ public class SessionEntity { private final String id; // 세션 아이디(UUID) private final long userId; // DB 키 private final String userRole; - private final String nickname; + private String nickname; private final long createdAt; private volatile long lastAccessAt; @@ -26,4 +26,8 @@ public SessionEntity(String id, long userId, String userRole, String nickname, l public String getNickname() { return nickname; } public long getLastAccessAt() { return lastAccessAt; } + + public void setNickname(String nickname) { + this.nickname = nickname; + } } diff --git a/src/main/resources/static/global.css b/src/main/resources/static/global.css index 233d795ba..fac2a45c8 100644 --- a/src/main/resources/static/global.css +++ b/src/main/resources/static/global.css @@ -151,6 +151,18 @@ height: 200px; border-radius: 9999px; background: #d9d9d9; + overflow: hidden; /* 추가: 박스 밖으로 나가면 자르기 */ + display: flex; /* 선택: 정렬 안정화 */ + align-items: center; /* 선택 */ + justify-content: center; /* 선택 */ +} + +.profile_image .profile { + width: 100%; + height: 100%; + display: block; + object-fit: cover; /* 꽉 채우기 (넘치는 부분은 잘림) */ + object-position: center; /* 중앙 기준 */ } .profile_container { diff --git a/src/main/resources/static/main/index.html b/src/main/resources/static/main/index.html deleted file mode 100644 index b5b91be02..000000000 --- a/src/main/resources/static/main/index.html +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - - - -
-
- - -
-
-
- - -
-
    -
  • - -
  • -
  • - -
  • -
- -
-

- 우리는 시스템 아키텍처에 대한 일관성 있는 접근이 필요하며, 필요한 - 모든 측면은 이미 개별적으로 인식되고 있다고 생각합니다. 즉, 응답이 - 잘 되고, 탄력적이며 유연하고 메시지 기반으로 동작하는 시스템 입니다. - 우리는 이것을 리액티브 시스템(Reactive Systems)라고 부릅니다. - 리액티브 시스템으로 구축된 시스템은 보다 유연하고, 느슨한 결합을 - 갖고, 확장성 이 있습니다. 이로 인해 개발이 더 쉬워지고 변경 사항을 - 적용하기 쉬워집니다. 이 시스템은 장애 에 대해 더 강한 내성을 지니며, - 비록 장애가 발생 하더라도, 재난이 일어나기 보다는 간결한 방식으로 - 해결합니다. 리액티브 시스템은 높은 응답성을 가지며 사용자 에게 - 효과적인 상호적 피드백을 제공합니다. -

-
-
    -
  • -
    - -

    account

    -
    -

    - 군인 또는 군무원이 아닌 국민은 대한민국의 영역안에서는 중대한 - 군사상 기밀·초병·초소·유독음식물공급·포로·군용물에 관한 죄중 - 법률이 정한 경우와 비상계엄이 선포된 경우를 제외하고는 군사법원의 - 재판을 받지 아니한다. -

    -
  • -
  • -
    - -

    account

    -
    -

    - 대통령의 임기연장 또는 중임변경을 위한 헌법개정은 그 헌법개정 제안 - 당시의 대통령에 대하여는 효력이 없다. 민주평화통일자문회의의 - 조직·직무범위 기타 필요한 사항은 법률로 정한다. -

    -
  • -
  • -
    - -

    account

    -
    -

    - 민주평화통일자문회의의 조직·직무범위 기타 필요한 사항은 법률로 - 정한다. -

    -
  • - - - - -
- -
-
- - diff --git a/src/main/resources/templates/article/index.html b/src/main/resources/templates/article/index.html index 41de15965..a78687a81 100644 --- a/src/main/resources/templates/article/index.html +++ b/src/main/resources/templates/article/index.html @@ -11,25 +11,45 @@ {{> /layout/header.html}}

게시글 작성

-
+

내용

+ +
+

파일 첨부

+ +
+
+ {{> /layout/error-popup.html}} diff --git a/src/main/resources/templates/comment/index.html b/src/main/resources/templates/comment/index.html index eca5bb38f..b81f55a94 100644 --- a/src/main/resources/templates/comment/index.html +++ b/src/main/resources/templates/comment/index.html @@ -11,25 +11,28 @@ {{> /layout/header.html}}

댓글 작성

-
+

내용

+
+ {{> /layout/error-popup.html}} diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 5c6a9545c..9843e6d32 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -12,76 +12,56 @@ {{> /layout/header.html}}
+ {{#if1 article}}
- +
  • - +
    + +
    +
  • +
  • +

    {{article.likeCount}}

  • +
  • +

    {{commentCount}}

    +

- 우리는 시스템 아키텍처에 대한 일관성 있는 접근이 필요하며, 필요한 - 모든 측면은 이미 개별적으로 인식되고 있다고 생각합니다. 즉, 응답이 - 잘 되고, 탄력적이며 유연하고 메시지 기반으로 동작하는 시스템 입니다. - 우리는 이것을 리액티브 시스템(Reactive Systems)라고 부릅니다. - 리액티브 시스템으로 구축된 시스템은 보다 유연하고, 느슨한 결합을 - 갖고, 확장성 이 있습니다. 이로 인해 개발이 더 쉬워지고 변경 사항을 - 적용하기 쉬워집니다. 이 시스템은 장애 에 대해 더 강한 내성을 지니며, - 비록 장애가 발생 하더라도, 재난이 일어나기 보다는 간결한 방식으로 - 해결합니다. 리액티브 시스템은 높은 응답성을 가지며 사용자 에게 - 효과적인 상호적 피드백을 제공합니다. + {{article.content}}

    + {{#each1 commentList}}
  • - -

    account

    -
    -

    - 군인 또는 군무원이 아닌 국민은 대한민국의 영역안에서는 중대한 - 군사상 기밀·초병·초소·유독음식물공급·포로·군용물에 관한 죄중 - 법률이 정한 경우와 비상계엄이 선포된 경우를 제외하고는 군사법원의 - 재판을 받지 아니한다. -

    -
  • -
  • -
    - -

    account

    -
    -

    - 대통령의 임기연장 또는 중임변경을 위한 헌법개정은 그 헌법개정 제안 - 당시의 대통령에 대하여는 효력이 없다. 민주평화통일자문회의의 - 조직·직무범위 기타 필요한 사항은 법률로 정한다. -

    -
  • -
  • -
    - -

    account

    + +

    {{this.nickname}}

    - 민주평화통일자문회의의 조직·직무범위 기타 필요한 사항은 법률로 - 정한다. + {{this.content}}

  • + {{/each1}} - + {{#if2 hasMoreComments}} + + 모든 댓글 보기(+{{additionalComments}}개) + + {{/if2}}
+ {{/if1}}