Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
22f4a06
ci: Claude 자동 코드 리뷰 워크플로우 추가
shinminkyoung1 Jan 12, 2026
3165f99
feat: H2 Database 의존성 추가
shinminkyoung1 Jan 13, 2026
6f749d2
feat: 유저 및 아티클 테이블 설계
shinminkyoung1 Jan 13, 2026
43bd76f
fix: schema.sql 파일명 오타 수정
shinminkyoung1 Jan 13, 2026
fb2916c
refactor: ConnectionManager의 DB 설정 정보를 Config 클래스로 분리
shinminkyoung1 Jan 13, 2026
6b207b4
feat: Article 모델 생성
shinminkyoung1 Jan 13, 2026
2cbce62
feat: Jdbc 기반의 ArticleDao 구현 및 DB 연동
shinminkyoung1 Jan 13, 2026
e0bf064
feat: AppConfig에 ArticleDao 등록 및 글쓰기 경로 매핑
shinminkyoung1 Jan 13, 2026
58576c6
refactor: 의존성 주입 체계에 article 기능 통합
shinminkyoung1 Jan 13, 2026
cb4038a
feat: article 작성을 위한 ArticleWriteHandler 구현
shinminkyoung1 Jan 13, 2026
213f203
feat: article/index.html 연결을 위한 경로 등록
shinminkyoung1 Jan 13, 2026
5864095
feat: 로그인한 유저 기반 게시글 저장 로직 구현
shinminkyoung1 Jan 13, 2026
1278e3d
feat: h2기반 유저 저장 로직 구현
shinminkyoung1 Jan 13, 2026
ea5fac8
refactor: article 작성 호출 url create->write로 변경
shinminkyoung1 Jan 13, 2026
2a592af
feat: 유저 기반 게시글 작성 및 동적 HTML 적용
shinminkyoung1 Jan 13, 2026
ba908a0
fix: TODO 목록 삭제
shinminkyoung1 Jan 13, 2026
9630dc8
refactor: 아티클 작성 폼 내 제목과 내용 규격 통일
shinminkyoung1 Jan 13, 2026
4974f96
fix: 아티클 저장 정상화를 위한 라우팅 수정
shinminkyoung1 Jan 13, 2026
940825b
feat: 아티클 작성 폼 내 이미지 첨부 칸 추가 및 멀티파트 설정
shinminkyoung1 Jan 13, 2026
7ef1a97
feat: MultipartPart 모델 구현
shinminkyoung1 Jan 13, 2026
fd9a6f8
feat: MultipartPart 파서 구현
shinminkyoung1 Jan 13, 2026
d5a6255
feat: Multipart 파서 도입 및 게시글 파일 업로드 기능 추가
shinminkyoung1 Jan 14, 2026
3b7e8db
fix: 스트림 읽기 방식 개선을 통한 broken pipe 에러 해결
shinminkyoung1 Jan 14, 2026
115a241
refactor: HTTP Request 바이트 스트림 기반으로 개편
shinminkyoung1 Jan 14, 2026
35b1104
refactor: HttpResponse 내 contentType get로직 개편
shinminkyoung1 Jan 14, 2026
a479181
feat: 이미지 서빙을 위한 pageRender 내 이미지 영역 추가
shinminkyoung1 Jan 14, 2026
231b35d
feat: MyPageHandler 구현
shinminkyoung1 Jan 14, 2026
b8a0cd4
feat: 프로필 이미지 변경 db 저장 로직 구현
shinminkyoung1 Jan 14, 2026
790cf8d
fix: 수정 프로필 이미지 업로드 경로 수정
shinminkyoung1 Jan 14, 2026
5b366c5
refactor: 데이터 메서드 전달을 위한 파일 판단 방식 변경
shinminkyoung1 Jan 14, 2026
17b3caa
refactor: 프로필 이미지 형식에 맞게 리사이징
shinminkyoung1 Jan 14, 2026
01e0e05
feat: 에러 전용 Html 페이지 구성
shinminkyoung1 Jan 14, 2026
ea671f7
feat: 커스텀 에러 페이지(404, 405, 500) 핸들링 기능 구현
shinminkyoung1 Jan 14, 2026
82c4d38
fix: 아티클 작성 정적 파일 재연결
shinminkyoung1 Jan 14, 2026
9be87fd
refactor: 아티클 페이지 내 로고 클릭 시 메인 페이지 이동
shinminkyoung1 Jan 14, 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
24 changes: 24 additions & 0 deletions .github/workflows/llm-code-review.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/db/ArticleDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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, imagePath) 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.setString(4, article.imagePath());

pstmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("Failed to save article: " + e.getMessage());
}
}

// 게시글 조회
public List<Article> selectAll() {
String sql = "SELECT * FROM ARTICLE ORDER BY createdAt DESC";
List<Article> 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(),
rs.getString("imagePath")
);
articles.add(article);
}
} catch (SQLException e) {
throw new RuntimeException("Failed to retrieve article list: {}", e);
}
return articles;
}
}
18 changes: 18 additions & 0 deletions src/main/java/db/ConnectionManager.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
77 changes: 77 additions & 0 deletions src/main/java/db/UserDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package db;

import model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class UserDao {
private static final Logger logger = LoggerFactory.getLogger(UserDao.class);

// 회원가입
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"),
rs.getString("profileImage")
);
}
}
} catch (SQLException e) {
throw new RuntimeException("Failed to find user", e);
}
return null;
}

// 정보 갱신
public void update(User user) {
String sql = "UPDATE USERS SET name = ?, email = ?, profileImage = ? WHERE userId = ?";

try (Connection connection = ConnectionManager.getConnection();
PreparedStatement pstmt = connection.prepareStatement(sql)) {

pstmt.setString(1, user.name());
pstmt.setString(2, user.email());
pstmt.setString(3, user.profileImage());
pstmt.setString(4, user.userId());

pstmt.executeUpdate();
logger.debug("User updated in DB: {}", user.userId());

} catch (SQLException e) {
logger.error("DB Update Error (User): {}", e.getMessage());
}
}
}
9 changes: 9 additions & 0 deletions src/main/java/model/Article.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package model;

import java.time.LocalDateTime;

public record Article(Long id, String writer, String title, String contents, LocalDateTime createdAt, String imagePath) {
public Article(String writer, String title, String contents, String imagePath) {
this(null, writer, title, contents, null, imagePath);
}
}
7 changes: 7 additions & 0 deletions src/main/java/model/MultipartPart.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package model;

public record MultipartPart (String name, String fileName, String contentType, byte[] data) {
public boolean isFile() {
return fileName != null;
}
}
2 changes: 1 addition & 1 deletion src/main/java/model/User.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package model;

public record User(String userId, String password, String name, String email) {
public record User(String userId, String password, String name, String email, String profileImage) {
}
57 changes: 55 additions & 2 deletions src/main/java/utils/HttpRequestUtils.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package utils;

import model.MultipartPart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import webserver.config.Pair;

import java.util.HashMap;
import java.util.Map;
import java.util.*;

public class HttpRequestUtils {
private static final Logger logger = LoggerFactory.getLogger(HttpRequestUtils.class);
Expand Down Expand Up @@ -111,4 +111,57 @@ public static Pair parseHeader(String headerLine) {
String value = headerLine.substring(index + 1).trim();
return new Pair(key, value);
}

// 멀티파트 파싱
public static List<MultipartPart> parseMultipartBody(byte[] body, String boundary) {
List<MultipartPart> parts = new ArrayList<>();
String delimiter = "--" + boundary;
byte[] delimiterBytes = delimiter.getBytes();

int start = 0;
while ((start = findIndex(body, delimiterBytes, start)) != -1) {
start += delimiterBytes.length;

if (start + 1 < body.length && body[start] == '-' && body[start + 1] == '-') break;

int headerEnd = findIndex(body, new byte[]{13, 10, 13, 10}, start);
if (headerEnd == -1) break;

String headerSection = new String(body, start + 2, headerEnd - start - 2);

String name = extractAttribute(headerSection, "name");
String fileName = extractAttribute(headerSection, "filename");
String contentType = extractAttribute(headerSection, "Content-Type");

int nextDelimiter = findIndex(body, delimiterBytes, headerEnd + 4);
if (nextDelimiter == -1) break;

byte[] partData = Arrays.copyOfRange(body, headerEnd + 4, nextDelimiter - 2);

parts.add(new MultipartPart(name, fileName, contentType, partData));
start = nextDelimiter;
}
return parts;
}

private static int findIndex(byte[] source, byte[] target, int start) {
for (int i = start; i <= source.length - target.length; i++) {
boolean match = true;
for (int j = 0; j < target.length; j++) {
if (source[i + j] != target[j]) {
match = false;
break;
}
}
if (match) return i;
}
return -1;
}

private static String extractAttribute(String header, String attr) {
int start = header.indexOf(attr + "=\"");
if (start == -1) return null;
start += attr.length() + 2;
return header.substring(start, header.indexOf("\"", start));
}
}
Loading