Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.backendboard.domain.user.dto.UserInfoResponse;
import com.backendboard.domain.user.dto.UserNicknameUpdateRequest;
import com.backendboard.domain.user.dto.UserNicknameUpdateResponse;
import com.backendboard.domain.user.dto.UserReadResponse;
import com.backendboard.domain.user.service.UserService;
import com.backendboard.global.error.dto.ErrorResponse;
import com.backendboard.global.security.dto.CustomUserDetails;
Expand All @@ -33,20 +33,18 @@ public class UserController {
private final UserService userService;

@Operation(
summary = "유저 보기 API",
description = "유저의 정보를 보여줍니다.",
summary = "내 정보 보기 API",
description = " 정보를 보여줍니다.",
security = {}
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "200 성공",
content = @Content(
mediaType = "application/json", schema = @Schema(implementation = UserReadResponse.class))),
@ApiResponse(responseCode = "404", description = "유저를 찾을 수 없습니다.",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
mediaType = "application/json", schema = @Schema(implementation = UserInfoResponse.class))),
})
@GetMapping("/{userId}")
public ResponseEntity<UserReadResponse> readUser(@PathVariable Long userId) {
UserReadResponse response = userService.getUser(userId);
@GetMapping
public ResponseEntity<UserInfoResponse> getInfo(@AuthenticationPrincipal CustomUserDetails customUserDetails) {
UserInfoResponse response = userService.getInfo(customUserDetails.getId());
return ResponseEntity.status(HttpStatus.OK).body(response);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

@Schema(description = "유저 보기 응답 DTO")
@Getter
public class UserReadResponse {
public class UserInfoResponse {
@Schema(description = "회원 고유 번호", example = "1")
private final Long id;

Expand All @@ -22,14 +22,14 @@ public class UserReadResponse {
private final String nickname;

@Builder
private UserReadResponse(Long id, String loginId, String username, String nickname) {
private UserInfoResponse(Long id, String loginId, String username, String nickname) {
this.id = id;
this.loginId = loginId;
this.username = username;
this.nickname = nickname;
}

public static UserReadResponse toDto(User user) {
public static UserInfoResponse toDto(User user) {
return builder()
.id(user.getId())
.loginId(user.getAuthUser().getUsername())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.backendboard.domain.user.service;

import com.backendboard.domain.user.dto.UserInfoResponse;
import com.backendboard.domain.user.dto.UserNicknameUpdateRequest;
import com.backendboard.domain.user.dto.UserNicknameUpdateResponse;
import com.backendboard.domain.user.dto.UserReadResponse;

public interface UserService {
UserReadResponse getUser(Long userId);
UserInfoResponse getInfo(Long authUserId);

UserNicknameUpdateResponse updateNickname(UserNicknameUpdateRequest request, Long userId, Long authUserId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.backendboard.domain.user.dto.UserInfoResponse;
import com.backendboard.domain.user.dto.UserNicknameUpdateRequest;
import com.backendboard.domain.user.dto.UserNicknameUpdateResponse;
import com.backendboard.domain.user.dto.UserReadResponse;
import com.backendboard.domain.user.entity.User;
import com.backendboard.domain.user.repository.UserRepository;
import com.backendboard.global.error.CustomError;
Expand All @@ -20,9 +20,9 @@ public class UserServiceImpl implements UserService {
private final UserRepository userRepository;

@Override
public UserReadResponse getUser(Long userId) {
User user = userRepository.findById(userId).orElseThrow(() -> new CustomException(CustomError.USER_NOT_FOUND));
return UserReadResponse.toDto(user);
public UserInfoResponse getInfo(Long authUserId) {
User user = userRepository.getByAuthUserId(authUserId);
return UserInfoResponse.toDto(user);
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.backendboard.global.config;

import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${spring.file.upload.directory}")
private String uploadDirectory;

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**")
.addResourceLocations("file:" + uploadDirectory + "/")
.setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.backendboard.global.config;

import static org.assertj.core.api.AssertionsForClassTypes.*;
import static org.junit.jupiter.api.Assumptions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;

@SpringBootTest
@AutoConfigureMockMvc
class WebConfigTest {
@Autowired
private MockMvc mockMvc;

@Value("${spring.file.upload.directory}")
private String uploadDirectory;

private static final String TEST_IMAGE_NAME = "test-image.jpg";

@BeforeEach
public void setup() throws IOException {
File directory = new File(uploadDirectory);
if (!directory.exists()) {
directory.mkdirs();
}

File testImage = new File(directory, TEST_IMAGE_NAME);
if (!testImage.exists()) {
BufferedImage image = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = image.createGraphics();
graphics.setColor(Color.RED);
graphics.fillRect(0, 0, 100, 100);
graphics.dispose();
ImageIO.write(image, "jpg", testImage);
}
}

@AfterEach
public void cleanup() {
File testImage = new File(uploadDirectory, TEST_IMAGE_NAME);
if (testImage.exists()) {
testImage.delete();
}
}

@DisplayName("이미지 파일이 존재하는지 테스트")
@Test
public void resourceHandlerConfiguration() throws Exception {
File imageFile = new File(uploadDirectory, TEST_IMAGE_NAME);
assumeTrue(imageFile.exists(), "테스트 이미지 파일이 존재해야 합니다: " + imageFile.getAbsolutePath());

mockMvc.perform(get("/images/" + TEST_IMAGE_NAME))
.andDo(print())
.andExpect(status().isOk())
.andExpect(header().exists(HttpHeaders.CONTENT_TYPE));
}

@DisplayName("캐시가 제대로 적용 되었는지 테스트")
@Test
public void cacheControlHeader() throws Exception {
File imageFile = new File(uploadDirectory, TEST_IMAGE_NAME);
assumeTrue(imageFile.exists(), "테스트 이미지 파일이 존재해야 합니다: " + imageFile.getAbsolutePath());

MvcResult result = mockMvc.perform(get("/images/" + TEST_IMAGE_NAME))
.andExpect(status().isOk())
.andExpect(header().exists(HttpHeaders.CACHE_CONTROL))
.andReturn();

String cacheControl = result.getResponse().getHeader(HttpHeaders.CACHE_CONTROL);
assertThat(cacheControl).contains("max-age=3600");
}

@DisplayName("이미지 파일이 존재하지 않은 경우")
@Test
public void nonExistentResource() throws Exception {
mockMvc.perform(get("/images/non-existent-image.jpg"))
.andExpect(status().isNotFound());
}

@DisplayName("상위 경로로 접근하는 경우")
@Test
public void directoryTraversalProtection() throws Exception {
mockMvc.perform(get("/images/../../../etc/passwd"))
.andExpect(status().isBadRequest());
}
}
4 changes: 3 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"bootstrap": "^5.3.5",
"bootstrap-icons": "^1.11.3",
"prismjs": "^1.30.0",
"quill": "^2.0.3",
"quill": "^1.3.7",
"quill-drag-and-drop-module": "^0.3.0",
"quill-image-resize-module-react": "^3.0.0",
"react": "^18.2.0",
"react-bootstrap": "^2.10.9",
"react-dom": "^18.2.0",
Expand Down
19 changes: 16 additions & 3 deletions frontend/src/pages/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, {useState} from 'react';
import {Alert, Button, Card, Col, Container, Form, Row} from 'react-bootstrap';
import LogoLink from "../components/LogoLink.jsx";
import {useNavigate} from "react-router-dom";
import api from "../api/axiosInstance.jsx";

const Login = () => {
const [email, setEmail] = useState('');
Expand Down Expand Up @@ -31,10 +32,22 @@ const Login = () => {
// 브라우저가 자동으로 multipart/form-data로 설정합니다.
});
if (response.ok) {
console.log('로그인 성공:');
localStorage.setItem('user', JSON.stringify({email, name: '감자 사용자'}));
localStorage.setItem('accessToken', response.headers.get('Authorization'));
navigate('/');
api.get('users').then(response => {
if (response.status === 200) {
console.log("성공적으로 데이터를 가져왔습니다.");
localStorage.setItem('user', JSON.stringify({
id: response.data.id,
email,
name: response.data.name,
nickname: response.data.nickname
}));
console.log('로그인 성공:');
navigate('/');
} else {
console.log(`오류 발생: ${response.status}`);
}
});
} else {
console.error('로그인 실패');
setShowError(true);
Expand Down
38 changes: 28 additions & 10 deletions frontend/src/pages/post/PostDetail.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import '../../style/PostDetil.css'

const PostDetail = () => {
const {id} = useParams();
const [nickname, setNickname] = useState('');
const [post, setPost] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
Expand All @@ -21,10 +22,15 @@ const PostDetail = () => {
const [hasMore, setHasMore] = useState(true);
const [editingComment, setEditingComment] = useState(null);
const [editCommentText, setEditCommentText] = useState('');

const commentsPerPage = 5;


useEffect(() => {
const user = localStorage.getItem('user');
if (user) {
setNickname(JSON.parse(user).nickname);
}
const accessToken = localStorage.getItem('accessToken');
setIsLoggedIn(!!user && !!accessToken);
}, []);
Expand All @@ -38,10 +44,21 @@ const PostDetail = () => {
})
.then(data => {
setPost(data);
setLikeCount(data.likeCount || 0);
api.get(`/post-likes/${id}/count`).then(res => {
if (res.status !== 200) {
throw new Error('좋아요 상태를 불러올 수 없습니다.');
}
setLikeCount(res.data.count);
})

// 로그인한 경우 좋아요 상태 확인
if (isLoggedIn) {
api.get(`/post-likes/${id}/status`).then(res => {
if (res.status !== 200) {
throw new Error('좋아요 상태를 불러올 수 없습니다.');
}
setIsLiked(res.data.liked);
})
// 좋아요 상태 확인 로직 추가
}
})
Expand All @@ -52,7 +69,7 @@ const PostDetail = () => {
// 댓글 불러오기
const fetchComments = (page = 0) => {
setCommentLoading(true);
api.get(`/comments/posts/${id}?page=${page}&size=${commentsPerPage}`)
api.get(`/comments/posts/${id}?page=${page}&size=${commentsPerPage}&sort=lastModifiedDate,desc`)
.then(res => {
// 응답 데이터 구조 확인
const responseData = res.data;
Expand All @@ -65,7 +82,7 @@ const PostDetail = () => {
}

// 더 불러올 댓글이 있는지 확인 (페이지네이션 정보 활용)
setHasMore(!res.data.empty);
setHasMore(!res.data.last);
})
.catch(err => {
console.error('댓글을 불러오는데 실패했습니다:', err);
Expand All @@ -84,7 +101,7 @@ const PostDetail = () => {

// 작성자와 현재 사용자가 일치하는지 확인하는 함수
const isPostAuthor = () => {
return true;
return post.author === nickname;
};

// 수정 페이지로 이동하는 핸들러
Expand Down Expand Up @@ -209,8 +226,7 @@ const PostDetail = () => {

// 댓글 작성자인지 확인
const isCommentAuthor = (comment) => {
// 실제 구현에서는 현재 로그인한 사용자와 댓글 작성자를 비교해야 함
return true; // 테스트를 위해 항상 true 반환
return comment.author === nickname;
};

if (loading) return <div>로딩 중...</div>;
Expand All @@ -223,12 +239,14 @@ const PostDetail = () => {
<h2 className="potato-title">{post.title}</h2>
</div>
<div className="mb-3">
<div className="text-muted small mb-2">
작성자: {post.author} | 좋아요: {post.likeCount} | 조회수: {post.viewCount}
<div className="text-muted small mb-2" style={{display: 'flex'}}>
작성자: {post.author} | 좋아요: {post.likeCount} |
조회수: {post.viewCount}
<span style={{marginLeft: 'auto'}}> {new Date(post.lastModifiedDate).toLocaleString()}</span>
</div>
{/* content가 HTML일 경우 안전하게 렌더링 */}
<div
className="potato-post-content"
className="ql-editor"
dangerouslySetInnerHTML={{__html: post.content}}
/>
</div>
Expand Down Expand Up @@ -296,7 +314,7 @@ const PostDetail = () => {
<div className="comment-header">
<span className="comment-author">{comment.author}</span>
<span className="comment-date">
{new Date(comment.createdAt).toLocaleString()}
{new Date(comment.lastModifiedDate).toLocaleString()}
</span>
</div>

Expand Down
Loading