Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
af4c3c7
feat: add 글쓰기 button in main page
Jan 14, 2026
39c0a70
fix: normalize request paths to resolve trailing slash issue
Jan 14, 2026
0d0a7bc
chore: update article.html
Jan 14, 2026
9b2ca26
feat: implement article creation skeleton to ArticleController
Jan 14, 2026
b3e553a
feat: add article model class
Jan 14, 2026
51691c2
feat: implement article management in Database and reorganize Databas…
Jan 14, 2026
cad49ac
feat: implement article creation and persistance in ArticleController
Jan 14, 2026
e8f8bea
feat: add defensive logic and logging for article content
Jan 14, 2026
67c0962
feat: enable article creation by mapping article route
Jan 14, 2026
595440a
feat: display latest articles on the main page
Jan 14, 2026
f5f2864
refactor: store articles in sorted order in Database
Jan 14, 2026
577259c
feat: add placeholders for article navigation and disabled states in …
Jan 14, 2026
5758f57
feat: add article navigation using query parameters and handle disabl…
Jan 14, 2026
ad59341
refactor: add defensive logic for non-existent article ID
Jan 14, 2026
471993d
docs: add today todo list
Jan 15, 2026
7b1a62c
feat: implement h2 database and refactor User data to use SQL queries
Jan 15, 2026
27d44a5
refactor: refactor Article data to use SQL queries
Jan 15, 2026
8fb5a84
fix: resolve stackoverflow error by moving user validation to controller
Jan 15, 2026
c1c8d0a
feat: add image upload button (html) and implement boundary parsing
Jan 15, 2026
f0105f0
refactor: implement byte-based parsing and multipart support
Jan 15, 2026
98b9106
Merge branch 'livebylee' into step7
livebylee Jan 16, 2026
cf0cbfc
feat: implement multipart body parsing and byte searching logic (inde…
Jan 16, 2026
93d5529
feat: complete multipart parsing and image save logic
Jan 16, 2026
5073cca
feat: implement image serving logic to display uploaded files
Jan 16, 2026
14436df
feat: implement unique file naming with UUID and refactor storage uti…
Jan 16, 2026
6a9a47f
feat: make image upload mandatory for article creation
Jan 16, 2026
500ddf9
fix: ensure getImagePath returns null when no file is uploaded
Jan 16, 2026
e75d365
fix: swap previous and next post
Jan 17, 2026
97606b5
fix: add error handling and alerts
Jan 17, 2026
b2ed186
Merge remote-tracking branch 'origin/step7' into step7
Jan 17, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,4 @@ gradle-app.setting
*.hprof

# End of https://www.toptal.com/developers/gitignore/api/gradle,intellij,java,macos

66 changes: 65 additions & 1 deletion STUDY3.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,68 @@

> ### JDBC (Java Database Connectivity)
>
> 데이터베이스에 연결하고 SQL 쿼리를 실행하는 표준 API
> 데이터베이스에 연결하고 SQL 쿼리를 실행하는 표준 API



-----
#### 0114 TODO (우선 글로만 게시글 올리는 기능 구현)
- [x] 글쓰기 버튼 추가
- [x] 사용자에 따른 글쓰기 페이지 접근 제한 처리
- [x] 폼에 입력된 데이터 받아오기
- [x] ArticleController
- [x] 데이터 타입 만들고 저장하기
- [x] controller mapping
- [x] 메인 화면에서 최신 게시글 보여주기
- [ ] db에 정렬된 상태로 들어가도록 수정하기

> #### `CopyOnWriteArrayList`
>
> 다수의 스레드에서 동시에 글이 작성된다면?
> - `Collections.synchronizedList` vs `CopyOnWriteArrayList`
> - 인스타그램을 모티브로한 현재 서비스에선 읽기가 훨씬 많지 않나?

> #### 글을 쓰는 대로 리스트에 추가되는 현재 상황에서 굳이 정렬이 필요한가?
>
> concurrency를 고려해보면 동시에 글을 올렸지만 네트워크 이슈 등의 문제로 작성 시간이 늦은 글이 더 먼저 추가될 수도 있을 듯


> #### 예외 처리 시점?
>
> `IndexController` 에서 게시글 ID를 전달받았을 때, 존재하지 않는 ID에 대한 예외 처리를 어느 계층에서 수행할지 있었다.
> 상위 메서드인 `process`에서 미리 검증을 수행하는 방식은
> 에러 상황을 조기에 발견하여 이후의 불필요한 비즈니스 로직 수행을 차단하는 관점에서 효율적이라고 생각했다.
> 실제 데이터를 읽어오는 하위 메서드인 `renderArticleSection`에서 직접 체크하는 방식의 경우, 검증 시점과 실제 데이터 사용 시점 사이의 차이가 작아 더 안전하다고 생각했다.
> 즉, `process`에서 ID의 유효성을 확인했더라도, 그 순간에 게시글이 삭제된다면 실제 렌더링 시점에는 예외가 발생할 수 있다. 결국 데이터 조회가 일어나는 시점과 검증 시점을 최대한 가깝게 해 데이터의 정합성을 높이는 것이 더 견고한 설계라고 판단했다.
> 따라서 하위 메서드에서 예외를 던지고 상위에서 이를 받아 처리하는 구조를 선택하기로 했다



-----
#### 0115 TODO
- [x] db 연동하기
- [ ] 이미지 업로드 모듈 (저장, 조회)
- [ ] 이미지 모듈 적용 - 글쓰기 페이지
- [ ] 이미지 모듈 적용 - 마이페이지
- [ ] 게시글 작성 기능 수정 (비즈니스 로직)
- [ ] 해당 내용 테스트
- [ ] javadoc
- [ ] + 로그인 실패 시 처리 수정

> ####
>
> DB 연동을 마치고 회원가입과 로그인 로직을 테스트하던 중 에러가 발생했다.
> 에러 로그를 보니 Database.findUserById와 User.<init>(생성자)이 서로를 무한하게 호출하는 문제가 있었다.
> 객체의 생성자 안에 Database.findUserById()를 호출하는 검증 로직을 넣었다.
> 하지만 Database.findUserById()는 DB에서 데이터를 찾으면 그 결과값으로 다시 new User()를 호출해 객체를 생성하려고 한다.
> -> 무한루프 발생
> 검증 로직을 생성자에서 빼서 Controller로 옮겨 해결하였다.
> 관심사 분리가 왜 중요한지...설계의 필요성.. 알게되었다.


> #### multipart reqeust
>
> `header` ) `content-type` ) `boundary`=
> : 브라우저가 랜덤생성한 구분자
>
> `Body` ) 시작바운더리 + 파트헤더 + \r\n + data + \r\n + 구분바운더리 + 헤더 + \r\n + imagedata + 종료바운더리
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ dependencies {
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'

implementation 'ch.qos.logback:logback-classic:1.2.3'
runtimeOnly 'com.h2database:h2:2.2.224'
testImplementation 'org.assertj:assertj-core:3.16.1'



}

test {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img_uploads/image1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions src/main/java/db/DBConnection.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package db;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DBConnection {
private static final String URL = "jdbc:h2:tcp://localhost/~/test";
private static final String USER = "sa";
private static final String PASSWORD = "";

public static Connection getConnection() {
try {
Class.forName("org.h2.Driver");
return DriverManager.getConnection(URL, USER, PASSWORD);
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
throw new RuntimeException("DB 연결 실패");
}
}

}
171 changes: 158 additions & 13 deletions src/main/java/db/Database.java
Original file line number Diff line number Diff line change
@@ -1,39 +1,184 @@
package db;

import model.Article;
import model.User;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import webserver.controller.CreateUserController;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.*;
import java.sql.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

public class Database {
private static Map<String, User> users = new HashMap<>();
private static Map<String, User> sessions = new HashMap<>();
private static final Logger logger = LoggerFactory.getLogger(Database.class);

private static Map<String, User> sessions = new ConcurrentHashMap<>();
//private static Map<String, Article> articles = new ConcurrentHashMap<>();
private static List<Article> sortedArticles = new CopyOnWriteArrayList<>();

// --- User 관련 ---
public static void addUser(User user) {
users.put(user.getUserId(), user);
String sql = "INSERT INTO users (user_id,password,name) VALUES (?, ?, ?)";

try (Connection connec = DBConnection.getConnection();
PreparedStatement pstmt = connec.prepareStatement(sql)) {
pstmt.setString(1, user.getUserId());
pstmt.setString(2, user.getPassword());
pstmt.setString(3, user.getName());

pstmt.executeUpdate();
logger.debug("[DB 저장 성공] user id :" + user.getUserId());
} catch (SQLException e) {
e.printStackTrace();
}

}

public static User findUserById(String userId) {
return users.get(userId);
String sql = "SELECT * FROM users WHERE user_id = ?";
try (Connection conn = DBConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {

pstmt.setString(1, userId);

try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) { // 결과가 있다면
return new User(
rs.getString("user_id"),
rs.getString("password"),
rs.getString("name")
);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}

public static User findUserByName(String name) {
return users.values().stream()
.filter(user -> user.getName().equals(name))
.findFirst()
.orElse(null);
String sql = "SELECT * FROM users WHERE name = ?";
try (Connection conn = DBConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, name);

try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return new User(
rs.getString("user_id"),
rs.getString("password"),
rs.getString("name")
);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}

public static Collection<User> findAll() {
return users.values();
List<User> userList = new ArrayList<>();
String sql = "SELECT * FROM users";
try (Connection conn = DBConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {

while (rs.next()) {
User user = new User(
rs.getString("user_id"),
rs.getString("password"),
rs.getString("name")
);
userList.add(user);
}
} catch (SQLException e) {
e.printStackTrace();
}
return userList;
}

// --- Session 관련 ---
public static void addSession(String sessionId, User user) {
sessions.put(sessionId, user);
}

public static User getUserBySessionId(String sessionId) {
if (sessionId == null) return null;
return sessions.get(sessionId);
}
}

// --- Article 관련 ---
public static void addArticle(Article article) {
String sql = "INSERT INTO ARTICLE (ID, AUTHOR_ID, CONTENT, IMAGE_PATH) VALUES (?, ?, ?, ?)";

try (Connection conn = DBConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {

pstmt.setString(1, article.getId());
pstmt.setString(2, article.getAuthorId());
pstmt.setString(3, article.getContent());
pstmt.setString(4, article.getImagePath());

pstmt.executeUpdate();
System.out.println(" [DB 저장 성공] 작성자: " + article.getAuthorId());

} catch (SQLException e) {
System.err.println(" [DB 저장 실패] error: " + e.getMessage());
e.printStackTrace();
}
}

public static Article findArticleById(String id) {
String sql = "SELECT * FROM ARTICLE WHERE ID = ?";

try (Connection conn = DBConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {

pstmt.setString(1, id);

try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return new Article(
rs.getString("ID"),
rs.getString("AUTHOR_ID"),
rs.getString("CONTENT"),
rs.getString("IMAGE_PATH"),
rs.getTimestamp("CREATED_AT").toLocalDateTime()
);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}

public static List<Article> findAllArticles() {
List<Article> articles = new ArrayList<>();
String sql = "SELECT * FROM ARTICLE ORDER BY CREATED_AT DESC";

try (Connection conn = DBConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {

while (rs.next()) {
articles.add(new Article(
rs.getString("ID"),
rs.getString("AUTHOR_ID"),
rs.getString("CONTENT"),
rs.getString("IMAGE_PATH"),
rs.getTimestamp("CREATED_AT").toLocalDateTime()
));
}
} catch (SQLException e) {
e.printStackTrace();
}
return articles;
}
}
Loading