diff --git a/.github/workflows/llm-code-review.yml b/.github/workflows/llm-code-review.yml new file mode 100644 index 000000000..ea0e34d74 --- /dev/null +++ b/.github/workflows/llm-code-review.yml @@ -0,0 +1,24 @@ +name: Claude Auto PR Review +on: + pull_request: + types: [opened, edited, synchronize] + +permissions: + contents: read + pull-requests: write + checks: write + id-token: write + +jobs: + review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Simple LLM Code Review + uses: codingbaraGo/simple-llm-code-review@latest + with: + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + language: korean \ No newline at end of file diff --git a/build.gradle b/build.gradle index 25dd8fcb7..3fe8c247b 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,8 @@ dependencies { implementation 'ch.qos.logback:logback-classic:1.2.3' testImplementation 'org.assertj:assertj-core:3.16.1' - + // h2 database + implementation 'com.h2database:h2:2.2.224' } test { diff --git a/src/main/java/db/ArticleDao.java b/src/main/java/db/ArticleDao.java new file mode 100644 index 000000000..a06676018 --- /dev/null +++ b/src/main/java/db/ArticleDao.java @@ -0,0 +1,52 @@ +package db; + +import model.Article; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +public class ArticleDao { + + // 게시글 저장 + public void insert(Article article) { + String sql = "INSERT INTO ARTICLE (writer, title, contents) VALUES (?, ?, ?)"; + + try (Connection connection = ConnectionManager.getConnection(); + PreparedStatement pstmt = connection.prepareStatement(sql)) { + + pstmt.setString(1, article.writer()); + pstmt.setString(2, article.title()); + pstmt.setString(3, article.contents()); + + pstmt.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Failed to save article: " + e.getMessage()); + } + } + + // 게시글 조회 + public List
selectAll() { + String sql = "SELECT * FROM ARTICLE ORDER BY createdAt DESC"; + List
articles = new ArrayList<>(); + + try (Connection connection = ConnectionManager.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + + while (rs.next()) { + Article article = new Article( + rs.getLong("id"), + rs.getString("writer"), + rs.getString("title"), + rs.getString("contents"), + rs.getTimestamp("createdAt").toLocalDateTime() + ); + articles.add(article); + } + } catch (SQLException e) { + throw new RuntimeException("Failed to retrieve article list: {}", e); + } + return articles; + } +} diff --git a/src/main/java/db/ConnectionManager.java b/src/main/java/db/ConnectionManager.java new file mode 100644 index 000000000..4ca0d8727 --- /dev/null +++ b/src/main/java/db/ConnectionManager.java @@ -0,0 +1,18 @@ +package db; + +import webserver.config.Config; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class ConnectionManager { + public static Connection getConnection() { + try { + Class.forName("org.h2.Driver"); + return DriverManager.getConnection(Config.DB_URL, Config.DB_USER, Config.DB_PW); + } catch (ClassNotFoundException | SQLException e) { + throw new RuntimeException("Failed to Connect SQL: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/db/UserDao.java b/src/main/java/db/UserDao.java new file mode 100644 index 000000000..576a0dfcd --- /dev/null +++ b/src/main/java/db/UserDao.java @@ -0,0 +1,53 @@ +package db; + +import model.User; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class UserDao { + + // 회원가입 + public void insert(User user) { + String sql = "INSERT INTO USERS (userId, name, password, email) VALUES (?, ?, ?, ?)"; + + try (Connection connection = ConnectionManager.getConnection(); + PreparedStatement pstmt = connection.prepareStatement(sql)) { + + pstmt.setString(1, user.userId()); + pstmt.setString(2, user.name()); + pstmt.setString(3, user.password()); + pstmt.setString(4, user.email()); + + pstmt.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Failed to save user", e); + } + } + + // ID로 유저 정보 찾기 + public User findUserById(String userId) { + String sql = "SELECT * FROM USERS WHERE userId = ?"; + + try (Connection connection = ConnectionManager.getConnection(); + PreparedStatement pstmt = connection.prepareStatement(sql)) { + + pstmt.setString(1, userId); + try (ResultSet rs = pstmt.executeQuery()) { + if (rs.next()) { + return new User( + rs.getString("userId"), + rs.getString("password"), + rs.getString("name"), + rs.getString("email") + ); + } + } + } catch (SQLException e) { + throw new RuntimeException("Failed to find user", e); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/model/Article.java b/src/main/java/model/Article.java new file mode 100644 index 000000000..562aa51fb --- /dev/null +++ b/src/main/java/model/Article.java @@ -0,0 +1,11 @@ +package model; + +import java.time.LocalDateTime; + +public record Article(Long id, String writer, String title, String contents, LocalDateTime createdAt) { + public Article(String writer, String title, String contents) { + this(null, writer, title, contents, null); + } +} + +// TODO writer -> writerId로 변경 \ No newline at end of file diff --git a/src/main/java/webserver/HttpResponse.java b/src/main/java/webserver/HttpResponse.java index 6f1af4591..e83caac80 100644 --- a/src/main/java/webserver/HttpResponse.java +++ b/src/main/java/webserver/HttpResponse.java @@ -49,7 +49,7 @@ public void sendRedirect(String redirectUrl) { processWrite(new byte[0]); // 바디 없음 } - public void fileResponse(String url, User loginUser) { + public void fileResponse(String url, User loginUser, Map additionalModel) { File file = new File(Config.STATIC_RESOURCE_PATH + url); if (!file.exists()) { sendError(HttpStatus.NOT_FOUND); @@ -66,6 +66,10 @@ public void fileResponse(String url, User loginUser) { Map model = new HashMap<>(); model.put("header_items", PageRender.renderHeader(loginUser)); + if (additionalModel != null) { + model.putAll(additionalModel); + } + content = TemplateEngine.render(content, model); body = content.getBytes(Config.UTF_8); @@ -122,4 +126,16 @@ private void setHttpHeader(String contentType, int contentLength) { addHeader("Content-Type", contentType + ";charset=" + Config.UTF_8); addHeader("Content-Length", String.valueOf(contentLength)); } + + public void sendHtmlContent(String content) { + try { + byte[] body = content.getBytes(Config.UTF_8); + this.status = HttpStatus.OK; + setHttpHeader("text/html", body.length); + processWrite(body); + } catch (Exception e) { + logger.error("Error while encoding HTML content: {}", e.getMessage()); + sendError(HttpStatus.INTERNAL_SERVER_ERROR); + } + } } \ No newline at end of file diff --git a/src/main/java/webserver/PageRender.java b/src/main/java/webserver/PageRender.java index 044e4c3cb..74675b2db 100644 --- a/src/main/java/webserver/PageRender.java +++ b/src/main/java/webserver/PageRender.java @@ -1,7 +1,10 @@ package webserver; +import model.Article; import model.User; +import java.util.List; + public class PageRender { public static String renderHeader(User loginUser) { StringBuilder sb = new StringBuilder(); @@ -35,4 +38,31 @@ public static String renderHeader(User loginUser) { } return sb.toString(); } + + public static String renderArticleList(List
articles) { + if (articles.isEmpty()) { + return "

등록된 게시글이 없습니다.

"; + } + + StringBuilder sb = new StringBuilder(); + for (Article article : articles) { + sb.append("
") + .append("
") + .append(" ") + .append(" ") + .append("
") + .append("
") + .append(article.title()) + .append("
") + .append("

").append(article.contents()).append("

") + .append("
") + .append("
    ") + .append("
  • ") + .append("
") + .append("
") + .append("
") + .append("
"); // 게시글 구분선 + } + return sb.toString(); + } } diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 2b3ad89af..4bcf2c036 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -4,6 +4,7 @@ import java.net.Socket; import db.Database; +import db.UserDao; import model.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,12 +16,12 @@ public class RequestHandler implements Runnable { private Socket connection; private final RouteGuide routeGuide; - private final Database database; + private final UserDao userDao; - public RequestHandler(Socket connectionSocket, RouteGuide routeGuide, Database database) { + public RequestHandler(Socket connectionSocket, RouteGuide routeGuide, UserDao userDao) { this.connection = connectionSocket; this.routeGuide = routeGuide; - this.database = database; + this.userDao = userDao; } public void run() { @@ -36,7 +37,7 @@ public void run() { // 유저 정보 추출 String sessionId = request.getCookie("sid"); - User loginUser = SessionManager.getLoginUser(sessionId, database); + User loginUser = SessionManager.getLoginUser(sessionId, userDao); String path = request.getPath(); if (path == null) return; @@ -55,7 +56,7 @@ public void run() { handler.process(request, response); } else { // 없으면 정적 파일 서빙 - response.fileResponse(path, loginUser); + response.fileResponse(path, loginUser, null); } } catch (IOException e) { diff --git a/src/main/java/webserver/SecurityInterceptor.java b/src/main/java/webserver/SecurityInterceptor.java index a1727916f..c51cb56c0 100644 --- a/src/main/java/webserver/SecurityInterceptor.java +++ b/src/main/java/webserver/SecurityInterceptor.java @@ -6,7 +6,7 @@ public class SecurityInterceptor { // 권한을 제한할 경로 - private static final List restrictedPaths = List.of("/mypage", "/user/logout"); + private static final List restrictedPaths = List.of("/mypage", "/user/logout", "article/write"); public static boolean preHandler(String path, User loginUser) { if (restrictedPaths.contains(path)) { diff --git a/src/main/java/webserver/SessionManager.java b/src/main/java/webserver/SessionManager.java index 2a58bb89b..92868c121 100644 --- a/src/main/java/webserver/SessionManager.java +++ b/src/main/java/webserver/SessionManager.java @@ -3,6 +3,7 @@ import db.Database; import db.SessionDatabase; import db.SessionEntry; +import db.UserDao; import model.User; import java.time.Duration; @@ -18,7 +19,7 @@ public static String createSession(User user) { return sessionId; } - public static User getSessionUser(String sessionId, Database database) { + public static User getSessionUser(String sessionId, UserDao userDao) { SessionEntry entry = SessionDatabase.find(sessionId); if (entry == null) return null; @@ -28,10 +29,10 @@ public static User getSessionUser(String sessionId, Database database) { } entry.updateLastAccessedTime(); - return database.findUserById(entry.getUserId()); + return userDao.findUserById(entry.getUserId()); } - public static User getLoginUser(String sessionId, Database database) { + public static User getLoginUser(String sessionId, UserDao userDao) { if (sessionId == null) { return null; } @@ -42,7 +43,7 @@ public static User getLoginUser(String sessionId, Database database) { } String userId = entry.getUserId(); - return database.findUserById(userId); + return userDao.findUserById(userId); } public static boolean isExpired(SessionEntry entry) { diff --git a/src/main/java/webserver/WebServer.java b/src/main/java/webserver/WebServer.java index 439fc5c07..4068edec6 100644 --- a/src/main/java/webserver/WebServer.java +++ b/src/main/java/webserver/WebServer.java @@ -9,6 +9,7 @@ import db.Database; import db.SessionDatabase; +import db.UserDao; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import webserver.config.AppConfig; @@ -32,7 +33,7 @@ public static void main(String args[]) throws Exception { port = Integer.parseInt(args[0]); } - Database database = AppConfig.getDatabase(); + UserDao userDao = AppConfig.getUserDao(); RouteGuide routeGuide = new RouteGuide(AppConfig.getRouteMappings()); // [백그라운드 작업] 만료된 세션 청소 @@ -45,7 +46,7 @@ public static void main(String args[]) throws Exception { // 클라이언트가 연결될때까지 대기한다. Socket connection; while ((connection = listenSocket.accept()) != null) { - executorService.execute(new RequestHandler(connection, routeGuide, database)); + executorService.execute(new RequestHandler(connection, routeGuide, userDao)); } } finally { executorService.shutdown(); diff --git a/src/main/java/webserver/config/AppConfig.java b/src/main/java/webserver/config/AppConfig.java index c401094b2..ff0e3a278 100644 --- a/src/main/java/webserver/config/AppConfig.java +++ b/src/main/java/webserver/config/AppConfig.java @@ -1,22 +1,26 @@ package webserver.config; +import db.ArticleDao; import db.Database; +import db.UserDao; +import model.Article; import model.User; import webserver.SessionManager; -import webserver.handler.Handler; -import webserver.handler.LoginRequestHandler; -import webserver.handler.LogoutRequestHandler; -import webserver.handler.UserRequestHandler; +import webserver.handler.*; import java.util.HashMap; import java.util.Map; public class AppConfig { - private static final Database database = new Database(); + private static final UserDao userDao = new UserDao(); + private static final ArticleDao articleDao = new ArticleDao(); - private static final Handler userHandler = new UserRequestHandler(database); - private static final Handler loginHandler = new LoginRequestHandler(database); - private static final Handler logoutHandler = new LogoutRequestHandler(database); + private static final Handler userHandler = new UserRequestHandler(userDao); + private static final Handler loginHandler = new LoginRequestHandler(userDao); + private static final Handler logoutHandler = new LogoutRequestHandler(userDao); + + private static final Handler articleWriteHandler = new ArticleWriteHandler(articleDao, userDao); + private static final Handler articleIndexHandler = new ArticleIndexHandler(articleDao); public static Map getRouteMappings() { Map mappings = new HashMap<>(); @@ -25,24 +29,28 @@ public static Map getRouteMappings() { mappings.put("/user/login", loginHandler); mappings.put("/user/logout", logoutHandler); + mappings.put("/article/write", articleWriteHandler); + mappings.put("/", articleIndexHandler); + mappings.put("/index.html", articleIndexHandler); + Map staticPages = Map.of( - "/", Config.DEFAULT_PAGE, "/registration", Config.REGISTRATION_PAGE, "/login", Config.LOGIN_PAGE, - "/mypage", Config.MY_PAGE + "/mypage", Config.MY_PAGE, + "/article", Config.ARTICLE_PAGE ); staticPages.forEach((path, filePath) -> mappings.put(path, (request, response) -> { String sessionId = request.getCookie("sid"); - User loginUser = SessionManager.getLoginUser(sessionId, database); - response.fileResponse(filePath, loginUser); + User loginUser = SessionManager.getLoginUser(sessionId, userDao); + response.fileResponse(filePath, loginUser, null); }) ); return mappings; } - public static Database getDatabase() { - return database; + public static UserDao getUserDao() { + return userDao; } } \ No newline at end of file diff --git a/src/main/java/webserver/config/Config.java b/src/main/java/webserver/config/Config.java index e36c4e459..371689f71 100644 --- a/src/main/java/webserver/config/Config.java +++ b/src/main/java/webserver/config/Config.java @@ -7,8 +7,15 @@ public class Config { public static final String LOGIN_PAGE = "/login/index.html"; public static final String MAIN_PAGE = "/main/index.html"; public static final String MY_PAGE = "/mypage/index.html"; + public static final String ARTICLE_PAGE = "/article/index.html"; public static final String UTF_8 = "utf-8"; public static final String CRLF = "\r\n"; public static final String HEADER_DELIMITER = ": "; + + // h2 database + // TODO: .gitignore로 관리 + public static final String DB_URL = "jdbc:h2:~/jwp-was;MODE=MySQL;AUTO_SERVER=TRUE"; + public static final String DB_USER = "apple"; + public static final String DB_PW = "1q2w3e4r"; } diff --git a/src/main/java/webserver/handler/ArticleIndexHandler.java b/src/main/java/webserver/handler/ArticleIndexHandler.java new file mode 100644 index 000000000..505a00239 --- /dev/null +++ b/src/main/java/webserver/handler/ArticleIndexHandler.java @@ -0,0 +1,54 @@ +package webserver.handler; + +import db.ArticleDao; +import db.UserDao; +import model.Article; +import model.User; +import org.h2.mvstore.Page; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import webserver.*; +import webserver.config.AppConfig; +import webserver.config.Config; +import webserver.config.HttpStatus; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ArticleIndexHandler implements Handler { + private static final Logger logger = LoggerFactory.getLogger(ArticleIndexHandler.class); + + private final ArticleDao articleDao; + + public ArticleIndexHandler(ArticleDao articleDao) { + this.articleDao = articleDao; + } + + @Override + public void process(HttpRequest request, HttpResponse response) { + String sessionId = request.getCookie("sid"); + User loginUser = SessionManager.getLoginUser(sessionId, AppConfig.getUserDao()); + + try { + File file = new File(Config.STATIC_RESOURCE_PATH + "/index.html"); + String content = new String(Files.readAllBytes(file.toPath()), Config.UTF_8); + + Map model = new HashMap<>(); + + model.put("header_items", PageRender.renderHeader(loginUser)); + + List
articles = articleDao.selectAll(); + model.put("posts_list", PageRender.renderArticleList(articles)); + + String renderedHtml = TemplateEngine.render(content, model); + + response.sendHtmlContent(renderedHtml); + } catch (IOException e) { + response.sendError(HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/webserver/handler/ArticleWriteHandler.java b/src/main/java/webserver/handler/ArticleWriteHandler.java new file mode 100644 index 000000000..73d5f9dfa --- /dev/null +++ b/src/main/java/webserver/handler/ArticleWriteHandler.java @@ -0,0 +1,46 @@ +package webserver.handler; + +import db.ArticleDao; +import db.UserDao; +import model.Article; +import model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import webserver.HttpRequest; +import webserver.HttpResponse; +import webserver.SessionManager; +import webserver.config.Config; + +public class ArticleWriteHandler implements Handler { + private static final Logger logger = LoggerFactory.getLogger(ArticleWriteHandler.class); + + private final UserDao userDao; + private final ArticleDao articleDao; + + public ArticleWriteHandler(ArticleDao articleDao, UserDao userDao) { + this.articleDao = articleDao; + this.userDao = userDao; + } + + @Override + public void process(HttpRequest request, HttpResponse response) { + String sessionId = request.getCookie("sid"); + User loginUser = SessionManager.getLoginUser(sessionId, userDao); + + if (loginUser == null) { + response.sendRedirect("/login"); + return; + } + + String title = request.getParameter("title"); + String contents = request.getParameter("contents"); + String writer = loginUser.userId(); + + Article article = new Article(writer, title, contents); + + articleDao.insert(article); + + logger.debug("Saved Article"); + response.sendRedirect(Config.DEFAULT_PAGE); + } +} diff --git a/src/main/java/webserver/handler/LoginRequestHandler.java b/src/main/java/webserver/handler/LoginRequestHandler.java index c6a28b1e3..defeaa9b9 100644 --- a/src/main/java/webserver/handler/LoginRequestHandler.java +++ b/src/main/java/webserver/handler/LoginRequestHandler.java @@ -1,6 +1,7 @@ package webserver.handler; import db.Database; +import db.UserDao; import model.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,10 +14,10 @@ public class LoginRequestHandler implements Handler { private static final Logger logger = LoggerFactory.getLogger(LoginRequestHandler.class); - private final Database database; + private final UserDao userDao; - public LoginRequestHandler(Database database) { - this.database = database; + public LoginRequestHandler(UserDao userDao) { + this.userDao = userDao; } @Override @@ -34,7 +35,7 @@ private void login(HttpRequest request, HttpResponse response) { String userId = request.getParameter("userId"); String password = request.getParameter("password"); - User user = database.findUserById(userId); + User user = userDao.findUserById(userId); // 유저 없는 경우 로그인 실패 if (user == null) { diff --git a/src/main/java/webserver/handler/LogoutRequestHandler.java b/src/main/java/webserver/handler/LogoutRequestHandler.java index 7c7d47c68..045677a2e 100644 --- a/src/main/java/webserver/handler/LogoutRequestHandler.java +++ b/src/main/java/webserver/handler/LogoutRequestHandler.java @@ -2,6 +2,7 @@ import db.Database; import db.SessionDatabase; +import db.UserDao; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import webserver.HttpRequest; @@ -11,10 +12,10 @@ public class LogoutRequestHandler implements Handler { private static final Logger logger = LoggerFactory.getLogger(LogoutRequestHandler.class); - private final Database database; + private final UserDao userDao; - public LogoutRequestHandler(Database database) { - this.database = database; + public LogoutRequestHandler(UserDao userDao) { + this.userDao = userDao; } @Override diff --git a/src/main/java/webserver/handler/UserRequestHandler.java b/src/main/java/webserver/handler/UserRequestHandler.java index 80dbe9adc..685afc960 100644 --- a/src/main/java/webserver/handler/UserRequestHandler.java +++ b/src/main/java/webserver/handler/UserRequestHandler.java @@ -1,6 +1,7 @@ package webserver.handler; import db.Database; +import db.UserDao; import model.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,10 +16,10 @@ public class UserRequestHandler implements Handler { public static final Logger logger = LoggerFactory.getLogger(UserRequestHandler.class); - private final Database database; + private final UserDao userDao; - public UserRequestHandler(Database database) { - this.database = database; + public UserRequestHandler(UserDao userDao) { + this.userDao = userDao; } @Override @@ -47,7 +48,7 @@ private void register(HttpRequest request, HttpResponse response) { } User user = new User(userId, password, name, email); - database.addUser(user); + userDao.insert(user); logger.debug("Saved User: {}", user); response.sendRedirect(Config.DEFAULT_PAGE); } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..31320a4b3 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,16 @@ +-- 회원 정보를 위한 테이블 +CREATE TABLE IF NOT EXISTS USERS ( + userId VARCHAR(50) PRIMARY KEY, + password VARCHAR(50) NOT NULL, + name VARCHAR(50) NOT NULL, + email VARCHAR(50) NOT NULL + ); + +-- 게시글 저장을 위한 테이블 +CREATE TABLE IF NOT EXISTS ARTICLE ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + writer VARCHAR(50) NOT NULL, + title VARCHAR(255) NOT NULL, + contents TEXT NOT NULL, + createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); \ No newline at end of file diff --git a/src/main/resources/static/article/index.html b/src/main/resources/static/article/index.html index 6d2c8eeef..148102dde 100644 --- a/src/main/resources/static/article/index.html +++ b/src/main/resources/static/article/index.html @@ -12,7 +12,7 @@
  • - 글쓰기 + 글쓰기