From af4c3c712795ad2bcc7ddb16e946638c553d0cec Mon Sep 17 00:00:00 2001 From: apple Date: Wed, 14 Jan 2026 14:32:28 +0900 Subject: [PATCH 01/28] =?UTF-8?q?feat:=20add=20=EA=B8=80=EC=93=B0=EA=B8=B0?= =?UTF-8?q?=20button=20in=20main=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- STUDY3.md | 10 +++++++++- src/main/resources/static/fragments/nav_logout.html | 7 +++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/STUDY3.md b/STUDY3.md index 6e5156727..99998c7e7 100644 --- a/STUDY3.md +++ b/STUDY3.md @@ -38,4 +38,12 @@ > ### JDBC (Java Database Connectivity) > -> 데이터베이스에 연결하고 SQL 쿼리를 실행하는 표준 API \ No newline at end of file +> 데이터베이스에 연결하고 SQL 쿼리를 실행하는 표준 API + + + +----- +#### 0114 TODO +- [x] 글쓰기 버튼 추가 +- [ ] 사용자에 따른 글쓰기 페이지 접근 제한 처리 +- \ No newline at end of file diff --git a/src/main/resources/static/fragments/nav_logout.html b/src/main/resources/static/fragments/nav_logout.html index 845677dd1..664440275 100644 --- a/src/main/resources/static/fragments/nav_logout.html +++ b/src/main/resources/static/fragments/nav_logout.html @@ -1,6 +1,9 @@
  • - {{userName}} 님 + 안녕하세요, {{userName}}님! +
  • +
  • + 글쓰기
  • 로그아웃 -
  • \ No newline at end of file + From 39c0a70bb23faff8663c5a8b7818de9806d40a94 Mon Sep 17 00:00:00 2001 From: apple Date: Wed, 14 Jan 2026 15:02:29 +0900 Subject: [PATCH 02/28] fix: normalize request paths to resolve trailing slash issue --- src/main/java/webserver/AuthChecker.java | 13 +++++-- .../controller/IndexControllerTest.java | 39 ++++++++++++------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/main/java/webserver/AuthChecker.java b/src/main/java/webserver/AuthChecker.java index 04679eab1..3dba5fe56 100644 --- a/src/main/java/webserver/AuthChecker.java +++ b/src/main/java/webserver/AuthChecker.java @@ -11,16 +11,22 @@ public class AuthChecker { private static final Logger logger = LoggerFactory.getLogger(AuthChecker.class); - private static final Set protectedPaths = Set.of("/mypage"); + private static final Set protectedPaths = Set.of("/mypage", "/article"); private static final Set guestOnlyPaths = Set.of("/login", "/registration"); // 인증이 필요한 경로인지 확인 public static boolean isProtectedPath(String path) { - return protectedPaths.contains(path); + String normalizedPath = path.endsWith("/") && path.length() > 1 + ? path.substring(0, path.length() - 1) + : path; + return protectedPaths.contains(normalizedPath); } public static boolean isGuestOnlyPath(String path) { - return guestOnlyPaths.contains(path); + String normalizedPath = path.endsWith("/") && path.length() > 1 + ? path.substring(0, path.length() - 1) + : path; + return guestOnlyPaths.contains(normalizedPath); } // 로그인 상태 확인 @@ -32,6 +38,7 @@ public static boolean isLoggedIn(HttpRequest request) { public static boolean checkAuthentication(HttpRequest request, HttpResponse response) { String path = request.getPath(); + if (isProtectedPath(path)) { if (!isLoggedIn(request)) { logger.debug("로그인하지 않은 사용자의 허용되지 않은 경로 접근 :{}", path); diff --git a/src/test/java/webserver/controller/IndexControllerTest.java b/src/test/java/webserver/controller/IndexControllerTest.java index 46050b73e..33c7e373b 100644 --- a/src/test/java/webserver/controller/IndexControllerTest.java +++ b/src/test/java/webserver/controller/IndexControllerTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import webserver.AuthChecker; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -24,7 +25,7 @@ class IndexControllerTest { void setUp() { indexController = new IndexController(); // 테스트 전용 유저 생성 및 DB 저장 - testUser = new User("tester", "password123", "테스터", "test@example.com"); + testUser = new User("tester", "password123", "테스터123", "test@example.com"); Database.addUser(testUser); } @@ -75,27 +76,35 @@ void index_authenticated() { @Test @DisplayName("로그인하지 않은 사용자가 마이페이지 접근 시 로그인 페이지로 리다이렉트 된다") void mypage_access_denied() { - // 이 테스트는 MyPageController 혹은 접근 제어 로직을 검증합니다. - // 여기서는 예시로 로직의 흐름을 보여줍니다. - - // 1. Given: 쿠키 없는 요청 - String requestString = "GET /mypage HTTP/1.1\r\nHost: localhost\r\n\r\n"; + String requestString = "GET /mypage/ HTTP/1.1\r\nHost: localhost\r\n\r\n"; HttpRequest request = new HttpRequest(new ByteArrayInputStream(requestString.getBytes())); ByteArrayOutputStream out = new ByteArrayOutputStream(); HttpResponse response = new HttpResponse(out); - // 2. When: 접근 제어 로직 수행 (보통 Controller 내부 혹은 Interceptor에서 수행) - String sid = request.getCookie("sid"); - User user = Database.getUserBySessionId(sid); - - if (user == null) { - response.sendRedirect("/login"); - } - - // 3. Then: 302 리다이렉트 응답 확인 + AuthChecker.checkAuthentication(request, response); String responseString = out.toString(StandardCharsets.UTF_8); assertTrue(responseString.contains("HTTP/1.1 302 Found"), "302 상태코드가 반환되어야 합니다."); assertTrue(responseString.contains("Location: /login"), "로그인 페이지로 리다이렉트 경로가 지정되어야 합니다."); } + + @Test + @DisplayName("로그인하지 않은 사용자가 글쓰기 페이지 접근 시 로그인 페이지로 리다이렉트 된다") + void articlepage_access_denied() { + // 1. Given: 로그인하지 않은 상태의 요청 생성 + String requestString = "GET /article/ HTTP/1.1\r\nHost: localhost\r\n\r\n"; + HttpRequest request = new HttpRequest(new ByteArrayInputStream(requestString.getBytes())); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + HttpResponse response = new HttpResponse(out); + + // 2. When: 실제 우리가 만든 AuthChecker의 로직을 실행! + // 테스트 코드에서 직접 if문을 쓰지 않고, 검증 대상인 AuthChecker를 호출합니다. + AuthChecker.checkAuthentication(request, response); + + // 3. Then: 결과 확인 + String responseString = out.toString(StandardCharsets.UTF_8); + assertTrue(responseString.contains("HTTP/1.1 302 Found"), "로그인이 안 되었으므로 302 응답이 와야 함"); + assertTrue(responseString.contains("Location: /login"), "리다이렉트 경로는 /login이어야 함"); + } } \ No newline at end of file From 0d0a7bc6f298f58f946cedd29cf547b3f2d191ea Mon Sep 17 00:00:00 2001 From: apple Date: Wed, 14 Jan 2026 15:12:41 +0900 Subject: [PATCH 03/28] chore: update article.html --- STUDY3.md | 6 ++++-- src/main/resources/static/article/index.html | 11 ++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/STUDY3.md b/STUDY3.md index 99998c7e7..4a2434894 100644 --- a/STUDY3.md +++ b/STUDY3.md @@ -43,7 +43,9 @@ ----- -#### 0114 TODO +#### 0114 TODO (우선 글로만 게시글 올리는 기능 구현) - [x] 글쓰기 버튼 추가 -- [ ] 사용자에 따른 글쓰기 페이지 접근 제한 처리 +- [x] 사용자에 따른 글쓰기 페이지 접근 제한 처리 +- [ ] 폼에 입력된 데이터 받아오기 +- [ ] 데이터 저장하기 - \ 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..e4692c70b 100644 --- a/src/main/resources/static/article/index.html +++ b/src/main/resources/static/article/index.html @@ -23,20 +23,21 @@

    게시글 작성

    -
    +

    내용

    From 9b2ca26455450e26303c7e1c7b880a93f5b89426 Mon Sep 17 00:00:00 2001 From: apple Date: Wed, 14 Jan 2026 15:32:18 +0900 Subject: [PATCH 04/28] feat: implement article creation skeleton to ArticleController --- src/main/java/webserver/controller/ArticleController.java | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/main/java/webserver/controller/ArticleController.java diff --git a/src/main/java/webserver/controller/ArticleController.java b/src/main/java/webserver/controller/ArticleController.java new file mode 100644 index 000000000..ef50cb3ae --- /dev/null +++ b/src/main/java/webserver/controller/ArticleController.java @@ -0,0 +1,4 @@ +package webserver.controller; + +public class ArticleController { +} From b3e553af3914a37855f467c3dcac582d5c0677a1 Mon Sep 17 00:00:00 2001 From: apple Date: Wed, 14 Jan 2026 15:40:19 +0900 Subject: [PATCH 05/28] feat: add article model class --- STUDY3.md | 5 ++- src/main/java/model/Article.java | 39 +++++++++++++++++++ src/main/java/webserver/RequestHandler.java | 2 - .../controller/ArticleController.java | 22 ++++++++++- 4 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 src/main/java/model/Article.java diff --git a/STUDY3.md b/STUDY3.md index 4a2434894..1dd8ab8eb 100644 --- a/STUDY3.md +++ b/STUDY3.md @@ -46,6 +46,7 @@ #### 0114 TODO (우선 글로만 게시글 올리는 기능 구현) - [x] 글쓰기 버튼 추가 - [x] 사용자에 따른 글쓰기 페이지 접근 제한 처리 -- [ ] 폼에 입력된 데이터 받아오기 -- [ ] 데이터 저장하기 +- [x] 폼에 입력된 데이터 받아오기 +- [ ] ArticleController +- [ ] 데이터 타입 만들고 저장하기 - \ 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..c75b314e7 --- /dev/null +++ b/src/main/java/model/Article.java @@ -0,0 +1,39 @@ +package model; + +import java.time.LocalDateTime; + +public class Article { + private Long id; + private String title; + private String content; + private String authorId; + private LocalDateTime createdAt; + + public Article(Long id, String title, String content, String authorId) { + this.id = id; + this.title = title; + this.content = content; + this.authorId = authorId; + this.createdAt = LocalDateTime.now(); + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getContent() { + return content; + } + + public String getAuthorId() { + return authorId; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } +} \ No newline at end of file diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 8b00f42f6..72627a288 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -2,9 +2,7 @@ import java.io.*; import java.net.Socket; -import java.util.Set; -import db.Database; import http.HttpMethod; import http.HttpRequest; import http.HttpResponse; diff --git a/src/main/java/webserver/controller/ArticleController.java b/src/main/java/webserver/controller/ArticleController.java index ef50cb3ae..323366b6b 100644 --- a/src/main/java/webserver/controller/ArticleController.java +++ b/src/main/java/webserver/controller/ArticleController.java @@ -1,4 +1,24 @@ package webserver.controller; -public class ArticleController { +import db.Database; +import http.HttpRequest; +import http.HttpResponse; +import webserver.AuthChecker; + +public class ArticleController implements Controller { + + public void process(HttpRequest request, HttpResponse response) { + + if (!AuthChecker.isLoggedIn(request)) { // 이미 로그인 된 사용자만 접근할 수 있는 페이지니까 필요 없나? + response.sendRedirect("/login"); + return; + } + + String content = request.getParams("content"); + String userId = Database.getUserBySessionId(request.getCookie("sid")).getUserId(); + + // 저장 + + // 리다이렉트 + } } From 51691c206589bf008add05d41168c618f163c89d Mon Sep 17 00:00:00 2001 From: apple Date: Wed, 14 Jan 2026 15:56:51 +0900 Subject: [PATCH 06/28] feat: implement article management in Database and reorganize Database class --- src/main/java/db/Database.java | 30 ++++++++++++++++++++++-------- src/main/java/model/Article.java | 14 +++++--------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/main/java/db/Database.java b/src/main/java/db/Database.java index 3f4d75fe8..2f114655d 100644 --- a/src/main/java/db/Database.java +++ b/src/main/java/db/Database.java @@ -1,15 +1,18 @@ package db; +import model.Article; import model.User; import java.util.Collection; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public class Database { - private static Map users = new HashMap<>(); - private static Map sessions = new HashMap<>(); + private static Map users = new ConcurrentHashMap<>(); + private static Map sessions = new ConcurrentHashMap<>(); + private static Map articles = new ConcurrentHashMap<>(); + // --- User 관련 --- public static void addUser(User user) { users.put(user.getUserId(), user); } @@ -19,16 +22,14 @@ public static User findUserById(String userId) { } public static User findUserByName(String name) { - return users.values().stream() - .filter(user -> user.getName().equals(name)) - .findFirst() - .orElse(null); + return users.get(name); } public static Collection findAll() { return users.values(); } + // --- Session 관련 --- public static void addSession(String sessionId, User user) { sessions.put(sessionId, user); } @@ -36,4 +37,17 @@ public static void addSession(String sessionId, User user) { public static User getUserBySessionId(String sessionId) { return sessions.get(sessionId); } -} + + // --- Article 관련 --- + public static void addArticle(Article article) { + articles.put(article.getId(), article); + } + + public static Article findArticleById(String id) { + return articles.get(id); + } + + public static Collection
    findAllArticles() { + return articles.values(); + } +} \ No newline at end of file diff --git a/src/main/java/model/Article.java b/src/main/java/model/Article.java index c75b314e7..5d3a01e9f 100644 --- a/src/main/java/model/Article.java +++ b/src/main/java/model/Article.java @@ -1,30 +1,26 @@ package model; import java.time.LocalDateTime; +import java.util.UUID; public class Article { - private Long id; + private String id; private String title; private String content; private String authorId; private LocalDateTime createdAt; - public Article(Long id, String title, String content, String authorId) { - this.id = id; - this.title = title; + public Article(String id, String title, String content, String authorId) { + this.id = UUID.randomUUID().toString(); this.content = content; this.authorId = authorId; this.createdAt = LocalDateTime.now(); } - public Long getId() { + public String getId() { return id; } - public String getTitle() { - return title; - } - public String getContent() { return content; } From cad49acd1570a5a41f3bc038f32e90744328b7e6 Mon Sep 17 00:00:00 2001 From: apple Date: Wed, 14 Jan 2026 16:05:23 +0900 Subject: [PATCH 07/28] feat: implement article creation and persistance in ArticleController - change Article class structure --- STUDY3.md | 4 ++-- src/main/java/model/Article.java | 2 +- src/main/java/webserver/controller/ArticleController.java | 7 +++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/STUDY3.md b/STUDY3.md index 1dd8ab8eb..52ca0f581 100644 --- a/STUDY3.md +++ b/STUDY3.md @@ -47,6 +47,6 @@ - [x] 글쓰기 버튼 추가 - [x] 사용자에 따른 글쓰기 페이지 접근 제한 처리 - [x] 폼에 입력된 데이터 받아오기 -- [ ] ArticleController -- [ ] 데이터 타입 만들고 저장하기 +- [x] ArticleController +- [x] 데이터 타입 만들고 저장하기 - \ No newline at end of file diff --git a/src/main/java/model/Article.java b/src/main/java/model/Article.java index 5d3a01e9f..889cad397 100644 --- a/src/main/java/model/Article.java +++ b/src/main/java/model/Article.java @@ -10,7 +10,7 @@ public class Article { private String authorId; private LocalDateTime createdAt; - public Article(String id, String title, String content, String authorId) { + public Article(String authorId, String content) { this.id = UUID.randomUUID().toString(); this.content = content; this.authorId = authorId; diff --git a/src/main/java/webserver/controller/ArticleController.java b/src/main/java/webserver/controller/ArticleController.java index 323366b6b..71fba3fb3 100644 --- a/src/main/java/webserver/controller/ArticleController.java +++ b/src/main/java/webserver/controller/ArticleController.java @@ -3,6 +3,8 @@ import db.Database; import http.HttpRequest; import http.HttpResponse; +import model.Article; +import model.User; import webserver.AuthChecker; public class ArticleController implements Controller { @@ -17,8 +19,9 @@ public void process(HttpRequest request, HttpResponse response) { String content = request.getParams("content"); String userId = Database.getUserBySessionId(request.getCookie("sid")).getUserId(); - // 저장 + Article article = new Article(userId, content); + Database.addArticle(article); - // 리다이렉트 + response.sendRedirect("/index.html"); } } From e8f8bea0b00312c8030effca58232683b9194a20 Mon Sep 17 00:00:00 2001 From: apple Date: Wed, 14 Jan 2026 16:16:50 +0900 Subject: [PATCH 08/28] feat: add defensive logic and logging for article content --- .../controller/ArticleController.java | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/main/java/webserver/controller/ArticleController.java b/src/main/java/webserver/controller/ArticleController.java index 71fba3fb3..eaf6340f1 100644 --- a/src/main/java/webserver/controller/ArticleController.java +++ b/src/main/java/webserver/controller/ArticleController.java @@ -5,22 +5,41 @@ import http.HttpResponse; import model.Article; import model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import webserver.AuthChecker; public class ArticleController implements Controller { + private static final Logger logger = LoggerFactory.getLogger(ArticleController.class); + private static final int MAX_CONTENT_LENGTH = 10000; public void process(HttpRequest request, HttpResponse response) { - if (!AuthChecker.isLoggedIn(request)) { // 이미 로그인 된 사용자만 접근할 수 있는 페이지니까 필요 없나? + if (!AuthChecker.isLoggedIn(request)) { + logger.debug("인증되지 않은 사용자의 접근"); response.sendRedirect("/login"); return; } String content = request.getParams("content"); - String userId = Database.getUserBySessionId(request.getCookie("sid")).getUserId(); + User user = Database.getUserBySessionId(request.getCookie("sid")); + + if (content.length() > MAX_CONTENT_LENGTH) { + logger.warn("content 길이 제한 초과: {} characters (max: {})", content.length(), MAX_CONTENT_LENGTH); + response.sendRedirect("/article"); + return; + } + if (user == null) { //세션 만료 대비 + logger.debug("Session expired or invalid for article creation"); + response.sendRedirect("/login"); + return; + } + + String userId = user.getUserId(); Article article = new Article(userId, content); Database.addArticle(article); + logger.debug("New article created by user: {}, content length: {}", userId, content.length()); response.sendRedirect("/index.html"); } From 67c0962394935d21e933a5a89c3386dbecd5f21f Mon Sep 17 00:00:00 2001 From: apple Date: Wed, 14 Jan 2026 16:24:49 +0900 Subject: [PATCH 09/28] feat: enable article creation by mapping article route --- STUDY3.md | 2 +- src/main/java/webserver/controller/RequestMapping.java | 1 + src/main/resources/static/article/index.html | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/STUDY3.md b/STUDY3.md index 52ca0f581..6236f5ec3 100644 --- a/STUDY3.md +++ b/STUDY3.md @@ -49,4 +49,4 @@ - [x] 폼에 입력된 데이터 받아오기 - [x] ArticleController - [x] 데이터 타입 만들고 저장하기 -- \ No newline at end of file +- [x] controller mapping \ No newline at end of file diff --git a/src/main/java/webserver/controller/RequestMapping.java b/src/main/java/webserver/controller/RequestMapping.java index 60e0850bd..8cbece976 100644 --- a/src/main/java/webserver/controller/RequestMapping.java +++ b/src/main/java/webserver/controller/RequestMapping.java @@ -16,6 +16,7 @@ private record MethodUrlKey(HttpMethod method, String url) { handlerMap.put(new MethodUrlKey(HttpMethod.POST, "/user/login"), new LoginUserController()); handlerMap.put(new MethodUrlKey(HttpMethod.GET, "/index.html"), new IndexController()); handlerMap.put(new MethodUrlKey(HttpMethod.GET, "/"), new IndexController()); + handlerMap.put(new MethodUrlKey(HttpMethod.POST, "/article/new"), new ArticleController()); } public static Controller getController(HttpMethod method, String url) { diff --git a/src/main/resources/static/article/index.html b/src/main/resources/static/article/index.html index e4692c70b..2c97278f8 100644 --- a/src/main/resources/static/article/index.html +++ b/src/main/resources/static/article/index.html @@ -23,7 +23,7 @@

    게시글 작성

    - +

    내용

    + +
    +

    이미지 첨부

    + +
    - + {{IMAGE_SECTION}}
    + diff --git a/src/main/resources/static/main.css b/src/main/resources/static/main.css index 54cbb844b..8601df009 100644 --- a/src/main/resources/static/main.css +++ b/src/main/resources/static/main.css @@ -12,6 +12,7 @@ flex-direction: column; align-items: start; gap: 16px; + word-break: break-all; } .post__account { @@ -47,6 +48,7 @@ font-size: 16px; line-height: 25.6px; color: #14212b; + word-break: break-all; } .post__menu {