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
ce6440d
feat: 메인 화면에서 변경된 프로필 띄워주기 기능
coli-geonwoo Jan 14, 2026
48fe26f
feat: article에 created_at 추가
coli-geonwoo Jan 14, 2026
c2d219b
feat: comment 도메인 정의
coli-geonwoo Jan 14, 2026
5985a90
feat: article id기반 이미지 조회 메서드 구현
coli-geonwoo Jan 14, 2026
7ab29b8
feat: 좋아요 객체 생성
coli-geonwoo Jan 14, 2026
6d971e2
feat: 좋아요 레포지토리 생성
coli-geonwoo Jan 14, 2026
edefc7f
feat: 댓글 레포지토리 생성
coli-geonwoo Jan 14, 2026
cfbb72f
feat: 댓글 조회 기능 추가
coli-geonwoo Jan 14, 2026
52b63f4
feat: api 응답을 잘 파싱하도록 js/css/html 코드 작성
coli-geonwoo Jan 14, 2026
8bc374e
feat: user.findById 메서드 구현
coli-geonwoo Jan 14, 2026
7b2e68f
refactor: 응답 dto 형식 변경
coli-geonwoo Jan 14, 2026
90b60f8
feat: db sql문 변경
coli-geonwoo Jan 14, 2026
ee42a14
feat: 좋아요를 글에 같이 쓸수 있도록 추가
coli-geonwoo Jan 14, 2026
851d098
feat: jackson 라이브러리 추가
coli-geonwoo Jan 14, 2026
fa2d423
docs: README.md 최신화
coli-geonwoo Jan 14, 2026
de088cb
feat: 모든 댓글보기 버튼 상황별 최신화 요구사항 만족
coli-geonwoo Jan 14, 2026
345301c
refactor: articleID도 같이 반환
coli-geonwoo Jan 14, 2026
c2e3caa
feat: comment 작성 기능 추가
coli-geonwoo Jan 14, 2026
2c3277b
feat: main/index.html도 비로그인 메인페이지와 같게 조정
coli-geonwoo Jan 15, 2026
fd4a78c
feat: JsonArgumentResolver 구현
coli-geonwoo Jan 15, 2026
84db4f4
feat: 댓글 작성 기능 구현
coli-geonwoo Jan 15, 2026
e1a2e83
feat: README.md 최신화
coli-geonwoo Jan 15, 2026
92c9bb9
feat: 빈 게시물 화면 생성
coli-geonwoo Jan 15, 2026
10dada3
feat: 좋아요 기능 구현
coli-geonwoo Jan 15, 2026
089126e
feat: 계정정보를 치환하도록 수정
coli-geonwoo Jan 15, 2026
a61afe8
rename: 이름 변경
coli-geonwoo Jan 15, 2026
780a9fc
refactor: 메인페이지 이동이 아니라 offset기반으로 요청되도록 수정
coli-geonwoo Jan 15, 2026
69b7ef7
docs: README.md 갱신
coli-geonwoo Jan 15, 2026
294adcd
refactor: 보고 있던 화면으로 이동 기능 구현
coli-geonwoo Jan 15, 2026
e419996
feat: 댓글 작성 시 보고 있던 화면으로 이동 기능 구현
coli-geonwoo Jan 15, 2026
877c540
fix: 회원가입 오류 개선
coli-geonwoo Jan 15, 2026
415b166
fix: 글쓰기 화면에서도 유저 정보 현출
coli-geonwoo Jan 15, 2026
8938f3f
fix: 글쓰기 실패 에러 해결
coli-geonwoo Jan 15, 2026
0d9c68b
fix: 비밀번호 인풋박스가 채워져 있지 않도록 수정
coli-geonwoo 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
45 changes: 22 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,22 @@
- [x] 로그인하지 않은 사용자가 글쓰기 버튼을 클릭하면 로그인 페이지로 이동한다.
- [x] write.html에서는 글의 본문을 입력할 수 있는 폼을 표시한다.
- [x] 작성한 글의 제목을 입력할 수 있는 칸을 넣는다
- [ ] 작성한 글의 제목을 index.html에서 보여준다.

### 메인 화면
- [ ] 로그인하지 않아도 볼수 있는 메인화면, 상단 GBN에는 서비스 로고, 로그인, 회원가입 버튼이 있다
- [ ] 화면에는 최신 마지막으로 업로드한 게시물의 이미지, 내용, 댓글이 있다
- [ ] 업로드를 한번도 한적이 없을 경우 빈 화면이 보일 수 있다
- [ ] 로그인 유무 상관없이 누구나 여러분 누를 때마다 +1 카운트 됨
- [ ] 댓글 아이콘 옆 숫자는 댓글에 달린 수만큼 카운트해서 보여줌
- [ ] 댓글
- [ ] 게시물 아래 댓글은 기본으로 최대 3개가 보여지고, 모든 댓글 보기 버튼을 누르면 작성된 모든 댓글들이 펼쳐짐
- [ ] 로그인하지 않은 사용자는 댓글을 달 수 없으며 댓글 작성 버튼을 누르면 로그인 화면으로 이동
- [ ] 모든 댓글보기 버튼 옆에는 노출된 3개를 포함한 전체 댓글 수가 보임
- [ ] 전체 댓글 숫자가 3개 이하면 모든 댓글 보기 버튼이 노출될 필요가 없음

- [ ] 댓글 아래쪽으로 `이전 글`, `댓글 작성`, `다음 글` 버튼이 있으며
- [ ] 최신 게시물 상태에서는 [다음 글] 버튼이 비활성화, [이전 글] 버튼은 활성화 되어 있음
- [ ] [다음 글], [이전 글] 버튼을 누르면 이전 글(더 예전에 작성된 글), 다음 글(더 최근에 작성된 글)로 각각 이동함
- [x] 로그인하지 않아도 볼수 있는 메인화면, 상단 GBN에는 서비스 로고, 로그인, 회원가입 버튼이 있다
- [x] 화면에는 최신 마지막으로 업로드한 게시물의 이미지, 내용, 댓글이 있다
- [x] 업로드를 한번도 한적이 없을 경우 빈 화면이 보일 수 있다
- [x] 로그인 유무 상관없이 누구나 여러번 좋아요를 누를 때마다 +1 카운트 됨
- [x] 댓글 아이콘 옆 숫자는 댓글에 달린 수만큼 카운트해서 보여줌
- [x] 댓글
- [x] 게시물 아래 댓글은 기본으로 최대 3개가 보여지고, 모든 댓글 보기 버튼을 누르면 작성된 모든 댓글들이 펼쳐짐
- [x] 로그인하지 않은 사용자는 댓글을 달 수 없으며 댓글 작성 버튼을 누르면 로그인 화면으로 이동
- [x] 모든 댓글보기 버튼 옆에는 노출된 3개를 포함한 전체 댓글 수가 보임
- [x] 전체 댓글 숫자가 3개 이하면 모든 댓글 보기 버튼이 노출될 필요가 없음

- [x] 댓글 아래쪽으로 `이전 글`, `댓글 작성`, `다음 글` 버튼이 있으며
- [x] 최신 게시물 상태에서는 [다음 글] 버튼이 비활성화, [이전 글] 버튼은 활성화 되어 있음
- [x] [다음 글], [이전 글] 버튼을 누르면 이전 글(더 예전에 작성된 글), 다음 글(더 최근에 작성된 글)로 각각 이동함

### 회원 가입
- [x] 아이디, 닉네임, 비밀번호는 모두 최소 4글자 이상으로 제한됨
Expand All @@ -93,20 +92,20 @@

### main/index.html
- [x] 안녕하세요, {닉네임}님! 이라는 메시지가 추가됨
- [ ] 댓글 작성 버튼을 누르면 댓글을 작성하는 화면으로 이동
- [ ] 본인 글 외에도 모든 회원가입자의 글이 보여야 함.
- [x] 댓글 작성 버튼을 누르면 댓글을 작성하는 화면으로 이동
- [x] 본인 글 외에도 모든 회원가입자의 글이 보여야 함.

### comment.html
- [ ] 로그인 된 사용자에 한정해서 댓글 작성 가능
- [ ] 작성완료를 누르면 보고 있던 게시물 화면으로 이동하게 됨
- [ ] 상단 GBM의 글쓰기를 누르면 새로운 게시글을 작성하는 화면으로 이동함
- [x] 로그인 된 사용자에 한정해서 댓글 작성 가능
- [x] 작성완료를 누르면 보고 있던 게시물 화면으로 이동하게 됨
- [x] 상단 GBM의 글쓰기를 누르면 새로운 게시글을 작성하는 화면으로 이동함

### write.html
- [x] 로그인 된 사용자에 한정해 사용가능한 글쓰기 화면
- [x] 내용 입력과 파일 첨부하는 화면
- [x] 파일 첨부는 필수(글을 작성해도 게시되지 않도록)
- [x] 파일이 첨부되어 있으면 글을 입력하지 않더라도 게시물 작성 가능
- [ ] 모든 내용 기입 후, [작성 완료]를 누르면 새로운 게시물 화면으로 이동 됨
- [x] 모든 내용 기입 후, [작성 완료]를 누르면 새로운 게시물 화면으로 이동 됨

### mypage.html
- [x] 로그인 된 사용자에 한정해 사용가능한 마이페이지 화면
Expand All @@ -125,11 +124,11 @@
- [x] 글쓰기 페이지에 이미지 파일 업로드 기능을 추가한다

### 이미지 보여주기 기능
- [ ] 메인 페이지에서 이미지를 보여주는 기능을 추가한다.
- [x] 메인 페이지에서 이미지를 보여주는 기능을 추가한다.

### mypage에 프로필 사진 변경 기능 추가
- [x] 로그인 상태에서 헤더의 사용자 id를 누를 경우 /mypage로 연결 > 이 때 기존에 등록된 기본 이미지를 변경해서 업로드
- [ ] 변경된 프로필 이미지는 메인 화면에서 볼 수 있어야 한다.
- [x] 변경된 프로필 이미지는 메인 화면에서 볼 수 있어야 한다.

### 404 에러 처리 페이지 구현
- [x] 존재하지 않는 요청에 대해 404 페이지로 이동하도록 구현
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ repositories {
dependencies {
implementation 'org.reflections:reflections:0.10.2' //reflection
runtimeOnly 'com.h2database:h2:2.2.224' //h2
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' //jackson

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/application/config/RepositoryConfig.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package application.config;

import application.repository.ArticleImageRepository;
import application.repository.ArticleLikesRepository;
import application.repository.ArticleRepository;
import application.repository.CommentRepository;
import application.repository.SessionRepository;
import application.repository.UserRepository;
import application.repository.impl.h2.ArticleH2Database;
import application.repository.impl.h2.ArticleImageH2Database;
import application.repository.impl.h2.CommentH2Database;
import application.repository.impl.h2.UserH2Database;
import application.repository.impl.memory.ArticleLikesMemoryRepository;
import application.repository.impl.memory.SessionMemoryDatabase;
import application.repository.rowmapper.SelectCommentsRowMapper;
import db.JdbcProperties;
import db.JdbcTemplate;

Expand All @@ -33,4 +38,12 @@ public static SessionRepository sessionRepository() {
public static ArticleImageRepository articleImageRepository() {
return new ArticleImageH2Database(jdbcTemplate);
}

public static ArticleLikesRepository articleLikesRepository() {
return new ArticleLikesMemoryRepository();
}

public static CommentRepository commentRepository() {
return new CommentH2Database(jdbcTemplate);
}
}
22 changes: 22 additions & 0 deletions src/main/java/application/dto/request/CommentCreateRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package application.dto.request;

public class CommentCreateRequest {

private long articleId;
private String content;

public CommentCreateRequest(long articleId, String content) {
this.articleId = articleId;
this.content = content;
}

private CommentCreateRequest() {}

public String getContent() {
return content;
}

public long getArticleId() {
return articleId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package application.dto.response;

public record ArticleCreateResponse(
long articleId,
long offSet
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package application.dto.response;

public record ArticleOffSetResponse(
long offSet,
long articleId
) {

}
15 changes: 15 additions & 0 deletions src/main/java/application/dto/response/CommentContentResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package application.dto.response;

import application.model.Comment;
import application.model.User;

public record CommentContentResponse(
String nickname,
String imageUrl,
String content
) {

public CommentContentResponse(User user, Comment comment) {
this(user.getName(), user.getImageUrl(), comment.getContent());
}
}
9 changes: 9 additions & 0 deletions src/main/java/application/dto/response/CommentResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package application.dto.response;

import java.util.List;

public record CommentResponse(
int count,
List<CommentContentResponse> contents
) {
}
35 changes: 35 additions & 0 deletions src/main/java/application/dto/response/LatestArticleResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package application.dto.response;

import application.model.Article;
import application.model.ArticleImage;
import application.model.User;
import java.util.List;

public record LatestArticleResponse(
int total,
long articleId,
String title,
String content,
String nickname,
String imageUrl,
List<String> images,
long likes,
CommentResponse comments

) {

public LatestArticleResponse(int total, Article article, User user, List<ArticleImage> images, long likes, CommentResponse commentResponse) {
this(
total,
article.getId(),
article.getTitle(),
article.getContent(),
user.getName(),
user.getImageUrl(),
images.stream().map(ArticleImage::getImageUrl).toList(),
likes,
commentResponse
);
}

}
7 changes: 7 additions & 0 deletions src/main/java/application/dto/response/LikesResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package application.dto.response;

public record LikesResponse(
long count
) {

}
2 changes: 2 additions & 0 deletions src/main/java/application/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ public enum ErrorCode {
LOGIN_FAILED(HttpStatusCode.UNAUTHORIZED_401, "비밀번호가 일치하지 않습니다"),
AUTHENTICATION_FAILED(HttpStatusCode.UNAUTHORIZED_401, "Authentication failed"),
USER_NOT_FOUND(HttpStatusCode.NOT_FOUND_404, "User not found"),
ARTICLE_LIKES_NOT_FOUND(HttpStatusCode.NOT_FOUND_404, "ARTICLES LIKES NOT FOUND"),
INVALID_ARTICLE_INPUT(HttpStatusCode.BAD_REQUEST_400, "Invalid article input"),
INVALID_MULTIPART_FILE(HttpStatusCode.BAD_REQUEST_400, "Invalid multipart file"),
INVALID_LATEST_ARTICLE_REQUEST(HttpStatusCode.BAD_REQUEST_400, "요청한 작성글 데이터가 없습니다."),

INVALID_VIEW_NAME_ERROR(HttpStatusCode.INTERNAL_SERVER_ERROR_500, "Wrong View Name : View Name is Null Or Empty"),
INTERNAL_SERVER_ERROR(HttpStatusCode.INTERNAL_SERVER_ERROR_500, "Internal Server Error"),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package application.exception;

import http.ContentType;
import http.HttpVersion;
import http.response.HttpResponse;
import http.response.HttpResponseBody;
Expand Down Expand Up @@ -33,7 +34,8 @@ public HttpResponse handleCustomException(CustomException customException) {
new HttpResponseHeader(new HashMap<>()),
null,
new HttpResponseBody(errorCode.getMessage().getBytes()),
null
null,
ContentType.HTML
);
}
}
53 changes: 49 additions & 4 deletions src/main/java/application/handler/ArticleHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@
import static application.config.argumentresolver.AuthMemberArgumentResolver.SESSION_ID_COOKIE_KEY;

import application.config.argumentresolver.AuthMember;
import application.dto.response.ArticleCreateResponse;
import application.dto.response.ArticleOffSetResponse;
import application.dto.response.LatestArticleResponse;
import application.dto.response.LikesResponse;
import application.model.User;
import application.service.ArticleFacadeService;
import application.service.AuthService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import http.ContentType;
import http.HttpMethod;
import http.request.HttpRequest;
import http.request.RequestCookie;
import http.response.HttpResponse;
import http.response.HttpResponseBody;
import http.response.ResponseCookie;
import webserver.argumentresolver.MultipartFiles;
import webserver.argumentresolver.RequestBody;
Expand All @@ -26,20 +34,57 @@ public class ArticleHandler {
public HttpResponse getArticle(HttpRequest request) {
if (request.hasCookie(SESSION_ID_COOKIE_KEY)) {
RequestCookie requestCookie = request.getRequestCookie();
authService.authroize(requestCookie.get(SESSION_ID_COOKIE_KEY));
return new HttpResponse("/article/index.html");
User user = authService.authroize(requestCookie.get(SESSION_ID_COOKIE_KEY));
HttpResponse response = new HttpResponse("/article/index.html");
response.addModelAttributes("account", user.getName());
response.addModelAttributes("profileImage", user.getImageUrl());
return response;
}
HttpResponse unAuthorizedResponse = HttpResponse.redirect("/login/index.html");
unAuthorizedResponse.setCookie(ResponseCookie.EXPIRED_RESPONSE_COOKIE);
return unAuthorizedResponse;
}

@RequestMapping(method = HttpMethod.GET, path= "/article/latest")
public HttpResponse getLatestArticle(HttpRequest request) {
String offset = request.getRequestParameter("offset");
LatestArticleResponse latestArticle = articleFacadeService.getLatestArticle(Integer.valueOf(offset));
String response = getJsonResponse(latestArticle);
return new HttpResponse(new HttpResponseBody(response.getBytes()), ContentType.JSON);
}

@RequestMapping(method = HttpMethod.POST, path = "/article")
public HttpResponse save(
@AuthMember User user,
@RequestBody MultipartFiles multipartFiles
) {
articleFacadeService.save(multipartFiles, user);
return HttpResponse.redirect("/"); //TODO 201로 전환
ArticleCreateResponse createResponse = articleFacadeService.save(multipartFiles, user);
String response = getJsonResponse(createResponse);
return new HttpResponse(new HttpResponseBody(response.getBytes()), ContentType.JSON);
}

@RequestMapping(method = HttpMethod.POST, path= "/article/likes")
public HttpResponse updateLikes(HttpRequest request) {
long articleId = Long.parseLong(request.getRequestParameter("articleId"));
LikesResponse likesResponse = articleFacadeService.incrementLikes(articleId);
String response = getJsonResponse(likesResponse);
return new HttpResponse(new HttpResponseBody(response.getBytes()), ContentType.JSON);
}

@RequestMapping(method = HttpMethod.GET, path= "/article/id")
public HttpResponse findById(HttpRequest request) {
long articleId = Long.parseLong(request.getRequestParameter("articleId"));
ArticleOffSetResponse articleOffset = articleFacadeService.getArticleOffSetById(articleId);
String response = getJsonResponse(articleOffset);
return new HttpResponse(new HttpResponseBody(response.getBytes()), ContentType.JSON);
}

private String getJsonResponse(Object object) {
try {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
50 changes: 50 additions & 0 deletions src/main/java/application/handler/CommentHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package application.handler;

import static application.config.argumentresolver.AuthMemberArgumentResolver.SESSION_ID_COOKIE_KEY;

import application.config.argumentresolver.AuthMember;
import application.dto.request.CommentCreateRequest;
import application.exception.CustomAuthException;
import application.model.User;
import application.service.AuthService;
import application.service.CommentService;
import http.HttpMethod;
import http.request.HttpRequest;
import http.request.RequestCookie;
import http.response.HttpResponse;
import webserver.argumentresolver.RequestBody;
import webserver.handler.HttpHandler;
import webserver.handler.RequestMapping;

@HttpHandler
public class CommentHandler {

private final AuthService authService = new AuthService();
private final CommentService commentService = new CommentService();

@RequestMapping(method = HttpMethod.GET, path = "/comment")
public HttpResponse commentPage(HttpRequest request) {
try {
if (!request.hasCookie(SESSION_ID_COOKIE_KEY)) {
throw new CustomAuthException("/login/index.html");
}
RequestCookie requestCookie = request.getRequestCookie();
User user = authService.authroize(requestCookie.get(SESSION_ID_COOKIE_KEY));
HttpResponse response = new HttpResponse("/comment/index.html");
response.addModelAttributes("account", user.getName());
response.addModelAttributes("profileImage", user.getImageUrl());
return response;
} catch (Exception e) {
return HttpResponse.redirect("/login");
}
}

@RequestMapping(method = HttpMethod.POST, path = "/comment")
public HttpResponse createComment(
@AuthMember User user,
@RequestBody CommentCreateRequest commentCreateRequest
) {
commentService.save(user, commentCreateRequest);
return HttpResponse.ok();
}
}
Loading