Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3382e37
refactor: FileUtil 을 전체 어플리케이션에서 활용할 수 있도록 외부 util 패키지로 분리:
kckc0608 Jan 14, 2026
add02b5
feat: 게시글에 사진 첨부시 사진을 저장하는 로직 작성
kckc0608 Jan 15, 2026
968c88e
feat: post image url 을 조회해서 html 에 설정하는 로직 작성
kckc0608 Jan 15, 2026
3616a47
fix: 이미지를 저장할 때 resource 폴더 경로를 제외하고 저장하도록 변경
kckc0608 Jan 15, 2026
7159b59
refactor: Part name, filename 파싱 로직을 정적 팩토리 메서드 내부로 이동
kckc0608 Jan 15, 2026
2e7afb5
refactor: post create handler 로직 리팩토링
kckc0608 Jan 15, 2026
6697f27
feat: post database에 게시글 전체 개수 조회 로직 작성
kckc0608 Jan 15, 2026
6db64f8
feat: 이전글, 다음글 로직 작성
kckc0608 Jan 15, 2026
5cabdae
refactor: content 가 없더라도 게시글을 작성할 수 있도록 로직 수정
kckc0608 Jan 15, 2026
9efa530
refactor: post 생성시 생성된 post 정보를 응답하도록 수정
kckc0608 Jan 15, 2026
9ad4bad
refactor: 게시글 생성시 생성된 게시글 화면으로 리다이렉트 되도록 수정
kckc0608 Jan 15, 2026
7d75c15
chore: 미사용 에러코드 제거
kckc0608 Jan 15, 2026
6336428
feat: 404 에러 발생시 404 에러 페이지로 리다이렉트 하도록 로직 작성
kckc0608 Jan 15, 2026
e108b1e
refactor: 로그인 아이디가 존재하지 않을 때 회원가입 페이지로 리다이렉트 할 수 있도록 confirm 띄우기
kckc0608 Jan 15, 2026
a48d1dc
chore: 명세에 맞게 안내 문구 수정
kckc0608 Jan 15, 2026
b16b4e2
refactor: 회원가입 폼에서 이메일 정보 제거
kckc0608 Jan 15, 2026
63e5601
feat: 회원가입 테스트에서 이메일 필드 삭제
kckc0608 Jan 15, 2026
d12d9d9
feat: 회원가입시 각 항목의 길이가 4글자 이상이 되도록 검증 추가
kckc0608 Jan 15, 2026
885c0a8
test: 회원가입 시 각 항목의 길이가 4글자 이상인지 검증하는 테스트 추가
kckc0608 Jan 15, 2026
f3008b0
feat: 회원가입에서 에러 발생시 해당 내용을 출력하도록 프론트 로직 작성
kckc0608 Jan 15, 2026
843ae8b
refactor: 회원가입 성공시 로그인 화면으로 리다이렉트 되도록 수정
kckc0608 Jan 15, 2026
ada721f
refactor: user 데이터베이스가 optional 로 동작하도록 수정, 닉네임으로 유저를 찾는 기능 추가
kckc0608 Jan 15, 2026
c6b207c
feat: 회원가입시 아이디, 닉네임 중복 검증 로직 작성
kckc0608 Jan 15, 2026
9e0c633
refactor: webserver 패키지 구조 리팩토링
kckc0608 Jan 15, 2026
5b5de04
refactor: AuthenticationGate 클래스 등록을 AppConfig 에서 하도록 수정
kckc0608 Jan 15, 2026
ff6967d
refactor: AuthenticationManager 의 위치를 webserver 외부로 분리
kckc0608 Jan 15, 2026
81b53cb
refactor: 인증 성공시 AuthenticationManager 안에 전역 인증 정보 저장 로직 작성
kckc0608 Jan 15, 2026
209ab47
fix: 유저 인증 필요 여부 검사 및 auth 세팅 로직 버그로 인한 무한 리다이렉트 문제 수정
kckc0608 Jan 15, 2026
7f2a45f
fix: config 내에서 의존관계가 존재할 때 주입이 제대로 되지 않는 버그 수정
kckc0608 Jan 15, 2026
b36b2ca
feat: 인증 정보를 전역 객체에서 가져와서 렌더링 하도록 수정
kckc0608 Jan 15, 2026
157398a
feat: 헤더에 mypage 이동 가능한 링크 추가
kckc0608 Jan 15, 2026
6fe0f6c
feat: 게시글 사용자 명에 마이페이지 이동 링크 삭제
kckc0608 Jan 15, 2026
7db7624
feat: 마이페이지 조회시 동적 렌더링된 페이지를 보도록 로직 수정
kckc0608 Jan 15, 2026
493a4e4
feat: 프로필에 기본 프로필 이미지가 적용되도록 수정
kckc0608 Jan 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/main/java/config/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@
import db.PostDatabase;
import db.UserDatabase;
import db.impl.UserMemoryDatabase;
import security.AuthenticationGate;

public class AppConfig {

public static AuthenticationGate getAuthenticationGate(UserDatabase userDatabase) {
return new AuthenticationGate(userDatabase);
}

public static PostDatabase getPostDatabase() {
return new PostH2Database();
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class SecurityConfig {
shouldAuthenticated.put("/article/index.html", TRUE);
shouldAuthenticated.put("/comment", TRUE);
shouldAuthenticated.put("/comment/index.html", TRUE);
shouldAuthenticated.put("/mypage", TRUE);
shouldAuthenticated.put("/mypage/index.html", TRUE);
}

public static Map<String, Boolean> getShouldAuthenticated() {
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/db/PostDatabase.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import java.util.Optional;

public interface PostDatabase {
void save(Post post);
Post save(Post post);
Optional<Post> findById(Long id);
List<Post> findAll();
Long countAll();
}
4 changes: 3 additions & 1 deletion src/main/java/db/UserDatabase.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import model.User;

import java.util.Collection;
import java.util.Optional;

public interface UserDatabase {
void addUser(User user);
User findUserById(String userId);
Optional<User> findUserById(String userId);
Optional<User> findUserByNickname(String nickname);
Collection<User> findAll();
void clear();
}
39 changes: 33 additions & 6 deletions src/main/java/db/impl/PostH2Database.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
Expand All @@ -19,27 +20,36 @@
public class PostH2Database implements PostDatabase {

@Override
public void save(Post post) {
public Post save(Post post) {
try (Connection connection = DriverManager.getConnection(DATABASE_HOST, DATABASE_USER, DATABASE_PASSWORD)) {
PreparedStatement statement;
if (post.getPostId() == null) {
statement = connection.prepareStatement("""
INSERT INTO post(content) VALUES (?)
""");
INSERT INTO post(content, image_url) VALUES (?, ?)
""", Statement.RETURN_GENERATED_KEYS);
statement.setString(1, post.getContent());
statement.setString(2, post.getImageUrl());
} else {
statement = connection.prepareStatement("""
UPDATE post
SET content = ?,
image_url = ?,
like_count = ?
WHERE post_id = ?
""");
statement.setString(1, post.getContent());
statement.setLong(2, post.getLikeCount());
statement.setLong(3, post.getPostId());
statement.setString(2, post.getImageUrl());
statement.setLong(3, post.getLikeCount());
statement.setLong(4, post.getPostId());
}

statement.executeUpdate();
try (ResultSet resultSet = statement.getGeneratedKeys()) {
if (resultSet.next()) {
return new Post(resultSet.getLong(1), post.getContent(), post.getImageUrl(), post.getLikeCount());
}
}
return post;
} catch (SQLException ex) {
throw new RuntimeException(ex);
}
Expand Down Expand Up @@ -89,10 +99,27 @@ public List<Post> findAll() {
}
}

@Override
public Long countAll() {
try (Connection connection = DriverManager.getConnection(DATABASE_HOST, DATABASE_USER, DATABASE_PASSWORD)) {
ResultSet resultSet = connection.createStatement().executeQuery("""
SELECT count(*) post_count FROM post
""");

if (resultSet.next()) {
return resultSet.getLong("post_count");
}
return 0L;
} catch (SQLException ex) {
throw new RuntimeException(ex);
}
}

private Post convertToPost(ResultSet resultSet) throws SQLException {
Long postId = resultSet.getLong("post_id");
String content = resultSet.getString("content");
String imageUrl = resultSet.getString("image_url");
Long likeCount = resultSet.getLong("like_count");
return new Post(postId, content, likeCount);
return new Post(postId, content, imageUrl, likeCount);
}
}
17 changes: 15 additions & 2 deletions src/main/java/db/impl/UserMemoryDatabase.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class UserMemoryDatabase implements UserDatabase {
private final Map<String, User> users = new HashMap<>();
Expand All @@ -14,8 +15,20 @@ public void addUser(User user) {
users.put(user.getUserId(), user);
}

public User findUserById(String userId) {
return users.get(userId);
public Optional<User> findUserById(String userId) {
if (users.containsKey(userId)) {
return Optional.of(users.get(userId));
}
return Optional.empty();
}

public Optional<User> findUserByNickname(String nickname) {
for (User user : users.values()) {
if (user.getName().equals(nickname)) {
return Optional.of(user);
}
}
return Optional.empty();
}

public Collection<User> findAll() {
Expand Down
15 changes: 12 additions & 3 deletions src/main/java/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,24 @@ public enum ErrorCode {
USER_REGISTRATION_FAILED_EMPTY_USERNAME(BAD_REQUEST, "아이디는 필수 항목입니다."),
USER_REGISTRATION_FAILED_EMPTY_PASSWORD(BAD_REQUEST, "비밀번호는 필수 항목입니다."),
USER_REGISTRATION_FAILED_EMPTY_NICKNAME(BAD_REQUEST, "닉네임은 필수 항목입니다."),
USER_REGISTRATION_FAILED_EMPTY_EMAIL(BAD_REQUEST, "이메일은 필수 항목입니다."),
USER_REGISTRATION_FAILED_SHORT_USERNAME(BAD_REQUEST, "아이디는 최소 4글자 이상이어야 합니다."),
USER_REGISTRATION_FAILED_SHORT_PASSWORD(BAD_REQUEST, "비밀번호는 최소 4글자 이상이어야 합니다."),
USER_REGISTRATION_FAILED_SHORT_NICKNAME(BAD_REQUEST, "닉네임은 최소 4글자 이상이어야 합니다."),
USER_REGISTRATION_FAILED_USERNAME_DUPLICATED(BAD_REQUEST, "이미 존재하는 아이디 입니다."),
USER_REGISTRATION_FAILED_NICKNAME_DUPLICATED(BAD_REQUEST, "이미 존재하는 닉네임 입니다."),

LOGIN_FAILED_WRONG_PASSWORD(NOT_AUTHORIZED, "비밀번호가 잘못되었습니다."),
LOGIN_FAILED_EMPTY_USERNAME(BAD_REQUEST, "아이디는 필수 항목입니다."),
LOGIN_FAILED_EMPTY_PASSWORD(BAD_REQUEST, "비밀번호는 필수 항목입니다."),

AUTHENTICATION_FAILED_USER_NOT_EXISTS(NOT_AUTHORIZED, "존재하지 않는 아이디 입니다."),
AUTHENTICATION_FAILED_WRONG_PASSWORD(NOT_AUTHORIZED, "비밀번호가 틀렸습니다."),

LOGOUT_FAILED_SESSION_KEY_NOT_FOUND(BAD_REQUEST, "요청에 세션 키가 존재하지 않습니다."),

POST_CREATE_FAILED_EMPTY_CONTENT(BAD_REQUEST, "content는 필수 항목입니다.");
POST_NOT_EXISTS(NOT_FOUND, "요청한 게시글이 존재하지 않습니다."),
POST_CREATE_FAILED_EMPTY_FILE(BAD_REQUEST, "게시글을 작성하려면 파일을 반드시 첨부해야 합니다."),

PAGE_NOT_ACCESSABLE_NOT_AUTHENTICATED(NOT_AUTHORIZED, "로그인 후에 접근 가능합니다.");

private final StatusCode statusCode;
private final String message;
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/exception/ExceptionHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import webserver.core.handler.IExceptionHandler;
import webserver.model.http.StatusCode;
import webserver.model.http.request.RequestHeader;
import webserver.model.http.response.Response;

public class ExceptionHandler implements IExceptionHandler {
Expand All @@ -11,6 +13,9 @@ public class ExceptionHandler implements IExceptionHandler {

public Response handleCustomException(CustomException ex) {
logger.error(ex.getMessage(), ex);
if (ex.getErrorCode().getStatusCode().equals(StatusCode.NOT_FOUND)) {
return Response.redirect((RequestHeader) null, "/404-not-found").build();
}
return Response.of(ex.getErrorCode().getStatusCode(), ex.getErrorCode().getMessage().getBytes());
}

Expand Down
13 changes: 6 additions & 7 deletions src/main/java/handler/HomePageHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

import db.PostDatabase;
import model.Post;
import model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import view.IndexView;
import webserver.auth.AuthenticationManager;
import security.AuthenticationManager;
import webserver.core.handler.DynamicHandler;
import webserver.core.handler.Handler;
import webserver.model.http.request.Request;
Expand All @@ -32,19 +33,17 @@ public String getHandlingPath() {

@Override
protected Response get(Request<Void> request) {
logger.info("[HomePageHandler] Get HomePage with request {}", request);
User currentUser = AuthenticationManager.getAuthentication(request).orElse(null);

String mimeType = StaticResourceUtil.convertFileExtensionToMIMEType(".html");
List<Post> posts = postDatabase.findAll();

if (posts.isEmpty()) {
String content = IndexView.renderEmpty(isLoggedIn(request));
String content = IndexView.renderEmpty(currentUser);
return Response.ok(request, mimeType, content.getBytes());
}

Post post = posts.get(0);
return Response.redirect(request, "/post?postId=" + post.getPostId()).build();
}

private boolean isLoggedIn(Request<Void> request) {
return AuthenticationManager.isAuthenticated(request);
}
}
12 changes: 5 additions & 7 deletions src/main/java/handler/LoginHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@

import static exception.ErrorCode.LOGIN_FAILED_EMPTY_PASSWORD;
import static exception.ErrorCode.LOGIN_FAILED_EMPTY_USERNAME;
import static exception.ErrorCode.AUTHENTICATION_FAILED_USER_NOT_EXISTS;
import static webserver.constant.WebServerConstant.COOKIE_SESSION_KEY;
import static webserver.constant.WebServerConstant.SESSION_TIMEOUT;
import static exception.ErrorCode.LOGIN_FAILED_WRONG_PASSWORD;
import static exception.ErrorCode.USER_NOT_EXISTS;
import static exception.ErrorCode.AUTHENTICATION_FAILED_WRONG_PASSWORD;

@Handler
public class LoginHandler extends DynamicHandler<String> {
Expand All @@ -43,14 +43,12 @@ protected Response post(Request<String> request) {
String username = formData.get("username");
String password = formData.get("password");

User user = userDatabase.findUserById(username);
if (user == null) {
throw new CustomException(USER_NOT_EXISTS);
}
User user = userDatabase.findUserById(username)
.orElseThrow(() -> new CustomException(AUTHENTICATION_FAILED_USER_NOT_EXISTS));

// TODO : 비밀번호 해시?
if (!user.getPassword().equals(password)) {
throw new CustomException(LOGIN_FAILED_WRONG_PASSWORD);
throw new CustomException(AUTHENTICATION_FAILED_WRONG_PASSWORD);
}

String sessionKey = SessionManager.createSession(user);
Expand Down
22 changes: 12 additions & 10 deletions src/main/java/handler/MyPageHandler.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package handler;

import webserver.auth.AuthenticationManager;
import exception.CustomException;
import model.User;
import security.AuthenticationManager;
import view.MyPageView;
import webserver.core.handler.DynamicHandler;
import webserver.core.handler.Handler;
import webserver.model.http.request.Request;
import webserver.model.http.response.Response;
import webserver.util.FileUtil;
import webserver.util.StaticResourceUtil;

import java.io.File;
import static exception.ErrorCode.PAGE_NOT_ACCESSABLE_NOT_AUTHENTICATED;

@Handler
public class MyPageHandler extends DynamicHandler<Void> {
Expand All @@ -20,12 +22,12 @@ public String getHandlingPath() {

@Override
protected Response get(Request<Void> request) {
if (AuthenticationManager.isAuthenticated(request)) {
File mypageFile = StaticResourceUtil.getStaticResourceFile(request.uri());
String mimeType = StaticResourceUtil.convertFileExtensionToMIMEType(mypageFile.getName());
byte[] fileContent = FileUtil.getFileContent(mypageFile);
return Response.ok(request, mimeType, fileContent);
}
return Response.redirect(request, "/login").build();
User currentUser = AuthenticationManager.getAuthentication(request).orElseThrow(
() -> new CustomException(PAGE_NOT_ACCESSABLE_NOT_AUTHENTICATED));

String mimeType = StaticResourceUtil.convertFileExtensionToMIMEType(".html");
String page = MyPageView.render(currentUser);

return Response.ok(request, mimeType, page.getBytes());
}
}
28 changes: 28 additions & 0 deletions src/main/java/handler/NotFoundPageHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package handler;

import model.User;
import view.error.NotFoundView;
import security.AuthenticationManager;
import webserver.core.handler.DynamicHandler;
import webserver.core.handler.Handler;
import webserver.model.http.request.Request;
import webserver.model.http.response.Response;
import webserver.util.StaticResourceUtil;

@Handler
public class NotFoundPageHandler extends DynamicHandler<Void> {

@Override
public String getHandlingPath() {
return "/404-not-found";
}

@Override
protected Response get(Request<Void> request) {
User currentUser = AuthenticationManager.getAuthentication(request).orElse(null);

String mimeType = StaticResourceUtil.convertFileExtensionToMIMEType(".html");
String page = NotFoundView.render(currentUser);
return Response.ok(request, mimeType, page.getBytes());
}
}
33 changes: 24 additions & 9 deletions src/main/java/handler/PostCreateHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@
import db.PostDatabase;
import exception.CustomException;
import model.Post;
import util.FileUtil;
import webserver.core.handler.DynamicHandler;
import webserver.core.handler.Handler;
import webserver.model.http.request.MultipartForm;
import webserver.model.http.request.Request;
import webserver.model.http.response.Response;
import webserver.util.HttpRequestParser;

import java.io.File;
import java.util.Map;

import static exception.ErrorCode.POST_CREATE_FAILED_EMPTY_CONTENT;
import static exception.ErrorCode.POST_CREATE_FAILED_EMPTY_FILE;
import static webserver.constant.WebServerConstant.STATIC_RESOURCE_PATH;

@Handler
public class PostCreateHandler extends DynamicHandler<MultipartForm> {
Expand All @@ -34,15 +36,28 @@ protected Response post(Request<MultipartForm> request) {

validatePostCreateForm(parts);

String content = new String(parts.get("content").data());
Post post = new Post(content);
postDatabase.save(post);
return Response.redirect(request, "/index.html").build();
String content = getContent(parts);
String fileName = parts.get("file").fileName();
byte[] fileContent = parts.get("file").data();

String filePath = "/img/" + fileName;
FileUtil.writeFile(new File(STATIC_RESOURCE_PATH + filePath), fileContent);

Post post = postDatabase.save(new Post(content, filePath));

return Response.redirect(request, "/post?postId=" + post.getPostId()).build();
}

private void validatePostCreateForm(Map<String, MultipartForm.Part> parts) {
if (!parts.containsKey("file") || parts.get("file").data() == null || parts.get("file").data().length == 0) {
throw new CustomException(POST_CREATE_FAILED_EMPTY_FILE);
}
}

private void validatePostCreateForm(Map<String, MultipartForm.Part> formData) {
if (!formData.containsKey("content")) {
throw new CustomException(POST_CREATE_FAILED_EMPTY_CONTENT);
private String getContent(Map<String, MultipartForm.Part> parts) {
if (parts.containsKey("content")) {
return new String(parts.get("content").data());
}
return "";
}
}
Loading