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..8814d0f01 --- /dev/null +++ b/src/main/java/db/ArticleDao.java @@ -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
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(), + rs.getString("imagePath") + ); + 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..54a02f096 --- /dev/null +++ b/src/main/java/db/UserDao.java @@ -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()); + } + } +} \ 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..854fafde2 --- /dev/null +++ b/src/main/java/model/Article.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/model/MultipartPart.java b/src/main/java/model/MultipartPart.java new file mode 100644 index 000000000..fe1703e60 --- /dev/null +++ b/src/main/java/model/MultipartPart.java @@ -0,0 +1,7 @@ +package model; + +public record MultipartPart (String name, String fileName, String contentType, byte[] data) { + public boolean isFile() { + return fileName != null; + } +} diff --git a/src/main/java/model/User.java b/src/main/java/model/User.java index 8881a3253..075f13103 100644 --- a/src/main/java/model/User.java +++ b/src/main/java/model/User.java @@ -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) { } \ No newline at end of file diff --git a/src/main/java/utils/HttpRequestUtils.java b/src/main/java/utils/HttpRequestUtils.java index b7c294349..ea3908d79 100644 --- a/src/main/java/utils/HttpRequestUtils.java +++ b/src/main/java/utils/HttpRequestUtils.java @@ -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); @@ -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 parseMultipartBody(byte[] body, String boundary) { + List 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)); + } } diff --git a/src/main/java/webserver/HttpRequest.java b/src/main/java/webserver/HttpRequest.java index 041b63e5e..44795266f 100644 --- a/src/main/java/webserver/HttpRequest.java +++ b/src/main/java/webserver/HttpRequest.java @@ -1,5 +1,6 @@ package webserver; +import model.MultipartPart; import model.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -7,14 +8,9 @@ import utils.IOUtils; import webserver.config.Pair; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; +import java.io.*; import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; -import java.util.Stack; +import java.util.*; public class HttpRequest { private static final Logger logger = LoggerFactory.getLogger(HttpRequest.class); @@ -27,6 +23,7 @@ public class HttpRequest { private Map cookies = new HashMap<>(); private Map headers = new HashMap<>(); private Map params = new HashMap<>(); + private List multipartParts = new ArrayList<>(); // 바디 길이를 저장 private int contentLength = 0; @@ -34,9 +31,8 @@ public class HttpRequest { private boolean isChunked = false; public HttpRequest(InputStream in) throws IOException { - BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); - String line = br.readLine(); + String line = readLine(in); if (line == null) return; // Request Line 파싱 @@ -51,36 +47,69 @@ public HttpRequest(InputStream in) throws IOException { } // 나머지 헤더 정보 읽음 - while ((line = br.readLine()) != null && !line.isEmpty()) { + while (true) { + line = readLine(in); + if (line == null || line.isEmpty()) { + break; + } + Pair pair = HttpRequestUtils.parseHeader(line); if (pair != null) { headers.put(pair.key, pair.value); - if ("Content-Length".equalsIgnoreCase(pair.key)) { this.contentLength = Integer.parseInt(pair.value); } - if ("Transfer-Encoding".equalsIgnoreCase(pair.key) && "chunked".equalsIgnoreCase(pair.value)) { - this.isChunked = true; - } - // 쿠키 파싱 if ("Cookie".equalsIgnoreCase(pair.key)) { this.cookies = HttpRequestUtils.parseCookies(pair.value); } } } - if (hasRequestBody()) { - if (isChunked) { - logger.debug("Body is chunked. Reading chunked data"); - String body = readChunkedBody(br); - this.params.putAll(HttpRequestUtils.parseParameters(body)); - } else if (contentLength > 0) { - String body = IOUtils.readData(br, contentLength); - this.params.putAll(HttpRequestUtils.parseParameters(body)); - } else { - logger.warn("POST request missing Content-Length or Transfer-Encoding. Path: {}", path); + if (contentLength > 0) { + parseBody(in); + } + } + + private String readLine(InputStream in) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int b; + while ((b = in.read()) != -1) { + if (b == '\n') break; + if (b != '\r') baos.write(b); + } + if (b == -1 && baos.size() == 0) return null; + return baos.toString(StandardCharsets.UTF_8); + } + + private void parseBody(InputStream in) throws IOException { + logger.debug("Starting parseBody. Content-Length to read: {}", contentLength); + + byte[] body = new byte[contentLength]; + int totalRead = 0; + while (totalRead < contentLength) { + int read = in.read(body, totalRead, contentLength - totalRead); + if (read == -1) break; + totalRead += read; + } + + String bodyStr = new String(body, StandardCharsets.UTF_8); + logger.debug("Raw Body content: [{}]", bodyStr); + + String contentType = headers.get("Content-Type"); + + if (contentType != null && contentType.contains("multipart/form-data")) { + String boundary = contentType.split("boundary=")[1]; + this.multipartParts = HttpRequestUtils.parseMultipartBody(body, boundary); + + for (MultipartPart part : multipartParts) { + if (!part.isFile()) { + params.put(part.name(), new String(part.data(), StandardCharsets.UTF_8)); + } } + + } else { + this.params.putAll(HttpRequestUtils.parseParameters(bodyStr)); } } @@ -149,4 +178,8 @@ public String getCookie(String name) { if (this.cookies == null) return null; return cookies.get(name); } + + public List getMultipartParts() { + return multipartParts; + } } \ No newline at end of file diff --git a/src/main/java/webserver/HttpResponse.java b/src/main/java/webserver/HttpResponse.java index 6f1af4591..63201f583 100644 --- a/src/main/java/webserver/HttpResponse.java +++ b/src/main/java/webserver/HttpResponse.java @@ -33,14 +33,25 @@ public void addHeader(String key, String value) { } public void sendError(HttpStatus status) { - if (isCommitted) { - logger.warn("The error page cannot be sent because the response has already started."); - return; - } + if (isCommitted) return; this.status = status; - byte[] body = status.getErrorMessageBytes(); - setHttpHeader("text/plain", body.length); - processWrite(body); + + String errorPagePath = "/error/" + status.getCode() + ".html"; + File file = new File(Config.STATIC_RESOURCE_PATH + errorPagePath); + + try { + byte[] body; + if (file.exists()) { + body = Files.readAllBytes(file.toPath()); + setHttpHeader("text/html", body.length); + } else { + body = status.getMessage().getBytes(Config.UTF_8); + setHttpHeader("text/plain", body.length); + } + processWrite(body); + } catch (IOException e) { + logger.error("Error while sending error page: {}", e.getMessage()); + } } public void sendRedirect(String redirectUrl) { @@ -49,8 +60,19 @@ 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() && !url.contains(".")) { + file = new File(Config.STATIC_RESOURCE_PATH + url + ".html"); + } + + if (file.isDirectory()) { + logger.warn("Request path is a directory: {}", url); + sendError(HttpStatus.NOT_FOUND); + return; + } + if (!file.exists()) { sendError(HttpStatus.NOT_FOUND); return; @@ -58,19 +80,28 @@ public void fileResponse(String url, User loginUser) { try { byte[] body = Files.readAllBytes(file.toPath()); + String fileName = file.getName(); // 동적 HTML 처리 - if (url.endsWith(".html")) { + if (fileName.endsWith(".html")) { String content = new String(body, Config.UTF_8); Map model = new HashMap<>(); model.put("header_items", PageRender.renderHeader(loginUser)); + if (additionalModel != null) { + model.putAll(additionalModel); + } + + logger.debug("Rendering HTML with model: {}", model.keySet()); + content = TemplateEngine.render(content, model); body = content.getBytes(Config.UTF_8); } - String contentType = MimeType.getContentType(HttpRequestUtils.getFileExtension(url)); + + String extension = HttpRequestUtils.getFileExtension(url); + String contentType = MimeType.getContentType(extension); this.status = HttpStatus.OK; setHttpHeader(contentType, body.length); processWrite(body); @@ -122,4 +153,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..d0e7f63af 100644 --- a/src/main/java/webserver/PageRender.java +++ b/src/main/java/webserver/PageRender.java @@ -1,7 +1,11 @@ package webserver; +import db.UserDao; +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 +39,59 @@ public static String renderHeader(User loginUser) { } return sb.toString(); } + + public static String renderArticleList(List
articles, UserDao userDao) { + if (articles.isEmpty()) { + return "

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

"; + } + + StringBuilder sb = new StringBuilder(); + for (Article article : articles) { + User writer = userDao.findUserById(article.writer()); + String profileImg = (writer != null) ? writer.profileImage() : "/img/basic_profileImage.svg"; + + sb.append("
"); + + // 작성자 정보 + sb.append("
"); + sb.append(" "); + sb.append(" "); + sb.append("
"); + + // 이미지 (imagePath 있을 경우만) + if (article.imagePath() != null && !article.imagePath().isEmpty()) { + sb.append(" "); + } + + // 좋아요, 공유, 북마크 + sb.append("
"); + sb.append("
    "); + sb.append("
  • "); + sb.append("
  • "); + sb.append("
"); + sb.append(" "); + sb.append("
"); + + // 4. 본문 내용 + sb.append("

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

"); + + sb.append("
"); + } + + return sb.toString(); + } + + public static String renderProfile(User user) { + StringBuilder sb = new StringBuilder(); + + String profileImage = (user.profileImage() != null && !user.profileImage().isEmpty()) + ? user.profileImage() + : "/img/basic_profileImage.svg"; + + sb.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..396ed2953 100644 --- a/src/main/java/webserver/config/AppConfig.java +++ b/src/main/java/webserver/config/AppConfig.java @@ -1,22 +1,29 @@ 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, userDao); + + private static final Handler myPageHandler = new MyPageHandler(userDao); + private static final Handler profileUpdateHandler = new ProfileUpdateHandler(userDao); public static Map getRouteMappings() { Map mappings = new HashMap<>(); @@ -25,24 +32,30 @@ 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); + + mappings.put("/mypage", myPageHandler); + mappings.put("/user/update", profileUpdateHandler); + Map staticPages = Map.of( - "/", Config.DEFAULT_PAGE, "/registration", Config.REGISTRATION_PAGE, "/login", Config.LOGIN_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..3f400182e --- /dev/null +++ b/src/main/java/webserver/handler/ArticleIndexHandler.java @@ -0,0 +1,56 @@ +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; + private final UserDao userDao; + + public ArticleIndexHandler(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, 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, userDao)); + + 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..eda63cfad --- /dev/null +++ b/src/main/java/webserver/handler/ArticleWriteHandler.java @@ -0,0 +1,79 @@ +package webserver.handler; + +import db.ArticleDao; +import db.UserDao; +import model.Article; +import model.MultipartPart; +import model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import webserver.HttpRequest; +import webserver.HttpResponse; +import webserver.SessionManager; +import webserver.config.Config; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.UUID; + +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(); + String imagePath = null; + + for (MultipartPart part : request.getMultipartParts()) { + if (part.isFile() && "image".equals(part.name()) && part.data().length > 0) { + imagePath = saveUploadedFile(part); + } + } + + Article article = new Article(writer, title, contents, imagePath); + + articleDao.insert(article); + + logger.debug("Saved Article"); + response.sendRedirect(Config.DEFAULT_PAGE); + } + + private String saveUploadedFile(MultipartPart part) { + String uploadDir = Config.STATIC_RESOURCE_PATH + "/uploads"; + File dir = new File(uploadDir); + if (!dir.exists()) { + dir.mkdir(); // 폴더 없으면 생성 + } + + String fileName = UUID.randomUUID().toString() + "_" + part.fileName(); + File file = new File(dir, fileName); + + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(part.data()); + logger.debug("File saved successfully: {}", file.getAbsolutePath()); + return "/uploads/" + fileName; + } catch (IOException e) { + logger.debug("File save error: {}", e.getMessage()); + return null; + } + } +} 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/MyPageHandler.java b/src/main/java/webserver/handler/MyPageHandler.java new file mode 100644 index 000000000..a522ee227 --- /dev/null +++ b/src/main/java/webserver/handler/MyPageHandler.java @@ -0,0 +1,38 @@ +package webserver.handler; + +import db.UserDao; +import model.User; +import webserver.HttpRequest; +import webserver.HttpResponse; +import webserver.PageRender; +import webserver.SessionManager; +import webserver.config.Config; + +import java.util.HashMap; +import java.util.Map; + +public class MyPageHandler implements Handler { + + private final UserDao userDao; + + public MyPageHandler (UserDao userDao) { + 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(Config.LOGIN_PAGE); + return; + } + + Map model = new HashMap<>(); + model.put("user_profile_image", PageRender.renderProfile(loginUser)); + model.put("user_name", loginUser.name()); + + response.fileResponse(Config.MY_PAGE, loginUser, model); + } +} diff --git a/src/main/java/webserver/handler/ProfileUpdateHandler.java b/src/main/java/webserver/handler/ProfileUpdateHandler.java new file mode 100644 index 000000000..58053512e --- /dev/null +++ b/src/main/java/webserver/handler/ProfileUpdateHandler.java @@ -0,0 +1,63 @@ +package webserver.handler; + +import db.UserDao; +import model.MultipartPart; +import model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import webserver.HttpRequest; +import webserver.HttpResponse; +import webserver.SessionManager; +import webserver.config.Config; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.UUID; + +public class ProfileUpdateHandler implements Handler { + private static final Logger logger = LoggerFactory.getLogger(ProfileUpdateHandler.class); + + private final UserDao userDao; + + public ProfileUpdateHandler(UserDao userDao) { + this.userDao = userDao; + } + + @Override + public void process(HttpRequest request, HttpResponse response) { + String sessionId = request.getCookie("sid"); + User loginUser = SessionManager.getLoginUser(sessionId, userDao); + + String profileImagePath = loginUser.profileImage(); + + for (MultipartPart part : request.getMultipartParts()) { + if (part.isFile() && "profileImage".equals(part.name()) && part.data().length > 0) { + profileImagePath = saveUploadedFile(part); + } + } + + User updatedUser = new User(loginUser.userId(), loginUser.password(), loginUser.name(), loginUser.email(), profileImagePath); + userDao.update(updatedUser); + + response.sendRedirect(Config.DEFAULT_PAGE); + } + + private String saveUploadedFile(MultipartPart part) { + String uploadDir = Config.STATIC_RESOURCE_PATH + "/uploads/"; + File dir = new File(uploadDir); + if (!dir.exists()) { + dir.mkdirs(); + } + + String fileName = UUID.randomUUID().toString() + "_" + part.fileName(); + File file = new File(dir, fileName); + + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(part.data()); + return "/uploads/" + fileName; + } catch (IOException e) { + return null; + } + } +} diff --git a/src/main/java/webserver/handler/UserRequestHandler.java b/src/main/java/webserver/handler/UserRequestHandler.java index 80dbe9adc..caf681d84 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 @@ -39,6 +40,8 @@ private void register(HttpRequest request, HttpResponse response) { String name = request.getParameter("name"); String email = request.getParameter("email"); + logger.debug("Parsing Check - ID: {}, PW: {}, Name: {}, Email: {}", userId, password, name, email); + // 유효성 검사 if (isAnyEmpty(userId, password, name)) { logger.warn("Registration failed: Missing required parameters."); @@ -46,8 +49,8 @@ private void register(HttpRequest request, HttpResponse response) { return; } - User user = new User(userId, password, name, email); - database.addUser(user); + User user = new User(userId, password, name, email, null); + 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..18a2204fc 100644 --- a/src/main/resources/static/article/index.html +++ b/src/main/resources/static/article/index.html @@ -9,10 +9,10 @@
- +

게시글 작성

-
+ +
+

제목

+ +

내용

+
+

이미지 첨부

+ +
diff --git a/src/main/resources/static/error/404.html b/src/main/resources/static/error/404.html new file mode 100644 index 000000000..cde069791 --- /dev/null +++ b/src/main/resources/static/error/404.html @@ -0,0 +1,65 @@ + + + + + + 404 - 페이지를 찾을 수 없습니다 + + + +
+

404

+

페이지를 찾을 수 없습니다

+

요청하신 페이지를 찾을 수 없습니다. 주소를 다시 확인하거나 메인 페이지로 이동해주세요.

+ 메인 페이지로 돌아가기 +
+ + \ No newline at end of file diff --git a/src/main/resources/static/error/405.html b/src/main/resources/static/error/405.html new file mode 100644 index 000000000..f8d808087 --- /dev/null +++ b/src/main/resources/static/error/405.html @@ -0,0 +1,65 @@ + + + + + + 405 - 허용되지 않는 요청 + + + +
+

405

+

허용되지 않는 요청 방식입니다

+

현재 페이지에서 사용 가능한 HTTP 메소드가 아닙니다. 올바른 방법으로 다시 시도해주세요.

+ 메인 페이지로 돌아가기 +
+ + \ No newline at end of file diff --git a/src/main/resources/static/error/500.html b/src/main/resources/static/error/500.html new file mode 100644 index 000000000..d2d06cd68 --- /dev/null +++ b/src/main/resources/static/error/500.html @@ -0,0 +1,65 @@ + + + + + + 500 - 서버 오류 + + + +
+

500

+

서버 내부 오류가 발생했습니다

+

현재 요청을 처리할 수 없습니다. 잠시 후 다시 시도하거나 관리자에게 문의해주세요.

+ 메인 페이지로 돌아가기 +
+ + \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 9e2446217..2a5134423 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -16,41 +16,7 @@
-
- - -
-
    -
  • - -
  • -
  • - -
  • -
- -
-

- 우리는 시스템 아키텍처에 대한 일관성 있는 접근이 필요하며, 필요한 - 모든 측면은 이미 개별적으로 인식되고 있다고 생각합니다. 즉, 응답이 - 잘 되고, 탄력적이며 유연하고 메시지 기반으로 동작하는 시스템 입니다. - 우리는 이것을 리액티브 시스템(Reactive Systems)라고 부릅니다. - 리액티브 시스템으로 구축된 시스템은 보다 유연하고, 느슨한 결합을 - 갖고, 확장성 이 있습니다. 이로 인해 개발이 더 쉬워지고 변경 사항을 - 적용하기 쉬워집니다. 이 시스템은 장애 에 대해 더 강한 내성을 지니며, - 비록 장애가 발생 하더라도, 재난이 일어나기 보다는 간결한 방식으로 - 해결합니다. 리액티브 시스템은 높은 응답성을 가지며 사용자 에게 - 효과적인 상호적 피드백을 제공합니다. -

+ {{posts_list}}
  • diff --git a/src/main/resources/static/mypage/index.html b/src/main/resources/static/mypage/index.html index 7ab744957..f88e57cd0 100644 --- a/src/main/resources/static/mypage/index.html +++ b/src/main/resources/static/mypage/index.html @@ -1,81 +1,111 @@ - - - - - - - -
    -
    - - -
    -
    -

    마이페이지

    + + + + + + + +
    +
    + + +
    +
    +

    마이페이지

    -
    -
    -
    - -
    -
    -
    -
    수정
    -
    -
    -
    -
    삭제
    -
    -
    -
    - -
    -

    닉네임

    - -
    -
    -

    비밀번호

    - + +
    +
    +
    + {{user_profile_image}} +
    + + + +
    +
    +
    수정
    -
    -

    비밀번호 확인

    - +
    +
    +
    삭제
    - +
    +
    + +
    +

    닉네임

    + +
    - +
    +

    비밀번호

    + +
    + +
    +

    비밀번호 확인

    + +
    + +
    -
    - - +
    +
    + + + + \ No newline at end of file diff --git a/src/main/resources/static/uploads/19db0a71-816c-4a1d-8280-71ca123c843b_IMG_0476.HEIC b/src/main/resources/static/uploads/19db0a71-816c-4a1d-8280-71ca123c843b_IMG_0476.HEIC new file mode 100644 index 000000000..762f2f66c Binary files /dev/null and b/src/main/resources/static/uploads/19db0a71-816c-4a1d-8280-71ca123c843b_IMG_0476.HEIC differ diff --git a/src/main/resources/static/uploads/2090159f-db66-4a32-a69c-e4db9d2b58ea_IMG_0955.jpeg b/src/main/resources/static/uploads/2090159f-db66-4a32-a69c-e4db9d2b58ea_IMG_0955.jpeg new file mode 100644 index 000000000..ed16085c7 Binary files /dev/null and b/src/main/resources/static/uploads/2090159f-db66-4a32-a69c-e4db9d2b58ea_IMG_0955.jpeg differ diff --git a/src/main/resources/static/uploads/5ceac905-ee63-497d-af59-e250da7ea1f4_IMG_0955.jpeg b/src/main/resources/static/uploads/5ceac905-ee63-497d-af59-e250da7ea1f4_IMG_0955.jpeg new file mode 100644 index 000000000..ed16085c7 Binary files /dev/null and b/src/main/resources/static/uploads/5ceac905-ee63-497d-af59-e250da7ea1f4_IMG_0955.jpeg differ diff --git a/src/main/resources/static/uploads/984aaf59-462c-417d-9514-224fb72ab79b_IMG_0955.jpeg b/src/main/resources/static/uploads/984aaf59-462c-417d-9514-224fb72ab79b_IMG_0955.jpeg new file mode 100644 index 000000000..ed16085c7 Binary files /dev/null and b/src/main/resources/static/uploads/984aaf59-462c-417d-9514-224fb72ab79b_IMG_0955.jpeg differ diff --git a/src/main/resources/static/uploads/a172f029-ad5c-494c-858c-9883aaebd9a8_IMG_0955.jpeg b/src/main/resources/static/uploads/a172f029-ad5c-494c-858c-9883aaebd9a8_IMG_0955.jpeg new file mode 100644 index 000000000..ed16085c7 Binary files /dev/null and b/src/main/resources/static/uploads/a172f029-ad5c-494c-858c-9883aaebd9a8_IMG_0955.jpeg differ diff --git a/src/main/resources/static/uploads/b0991fac-0cb8-48a1-a003-6f47a402e328_IMG_0955.jpeg b/src/main/resources/static/uploads/b0991fac-0cb8-48a1-a003-6f47a402e328_IMG_0955.jpeg new file mode 100644 index 000000000..ed16085c7 Binary files /dev/null and b/src/main/resources/static/uploads/b0991fac-0cb8-48a1-a003-6f47a402e328_IMG_0955.jpeg differ diff --git a/src/main/resources/static/uploads/b689ace8-a2a6-4156-a812-4a2b2c62ef51_IMG_0476.HEIC b/src/main/resources/static/uploads/b689ace8-a2a6-4156-a812-4a2b2c62ef51_IMG_0476.HEIC new file mode 100644 index 000000000..762f2f66c Binary files /dev/null and b/src/main/resources/static/uploads/b689ace8-a2a6-4156-a812-4a2b2c62ef51_IMG_0476.HEIC differ diff --git a/src/main/resources/static/uploads/bae48c8d-4941-4429-80c8-177679b159f6_IMG_0955.jpeg b/src/main/resources/static/uploads/bae48c8d-4941-4429-80c8-177679b159f6_IMG_0955.jpeg new file mode 100644 index 000000000..ed16085c7 Binary files /dev/null and b/src/main/resources/static/uploads/bae48c8d-4941-4429-80c8-177679b159f6_IMG_0955.jpeg differ diff --git "a/src/main/resources/static/uploads/cd40272d-9a78-4be6-944c-28cf8f8b3bef_\354\212\244\355\201\254\353\246\260\354\203\267 2026-01-09 \354\230\244\355\233\204 4.09.14.png" "b/src/main/resources/static/uploads/cd40272d-9a78-4be6-944c-28cf8f8b3bef_\354\212\244\355\201\254\353\246\260\354\203\267 2026-01-09 \354\230\244\355\233\204 4.09.14.png" new file mode 100644 index 000000000..67b7b17b4 Binary files /dev/null and "b/src/main/resources/static/uploads/cd40272d-9a78-4be6-944c-28cf8f8b3bef_\354\212\244\355\201\254\353\246\260\354\203\267 2026-01-09 \354\230\244\355\233\204 4.09.14.png" differ diff --git a/src/main/resources/static/uploads/dd5bd958-75bd-4e30-a412-d0017b79a725_IMG_0476.HEIC b/src/main/resources/static/uploads/dd5bd958-75bd-4e30-a412-d0017b79a725_IMG_0476.HEIC new file mode 100644 index 000000000..762f2f66c Binary files /dev/null and b/src/main/resources/static/uploads/dd5bd958-75bd-4e30-a412-d0017b79a725_IMG_0476.HEIC differ diff --git a/src/main/resources/static/uploads/f3f71d5e-4998-4f6e-9760-50257280e745_IMG_0476.HEIC b/src/main/resources/static/uploads/f3f71d5e-4998-4f6e-9760-50257280e745_IMG_0476.HEIC new file mode 100644 index 000000000..762f2f66c Binary files /dev/null and b/src/main/resources/static/uploads/f3f71d5e-4998-4f6e-9760-50257280e745_IMG_0476.HEIC differ diff --git a/src/main/resources/static/uploads/fbe1cbd6-bc62-4020-bd59-c0b1c70aa3ff_IMG_0476.HEIC b/src/main/resources/static/uploads/fbe1cbd6-bc62-4020-bd59-c0b1c70aa3ff_IMG_0476.HEIC new file mode 100644 index 000000000..762f2f66c Binary files /dev/null and b/src/main/resources/static/uploads/fbe1cbd6-bc62-4020-bd59-c0b1c70aa3ff_IMG_0476.HEIC differ