Skip to content

Commit ba2bd6d

Browse files
authored
Merge pull request #191 from Ureca-Mini-Project-Team4/feature/page-comment
Feature/page comment
2 parents 21e17d8 + 4f058fa commit ba2bd6d

File tree

12 files changed

+201
-85
lines changed

12 files changed

+201
-85
lines changed
6.76 KB
Loading

react-app/src/apis/comment/postComment.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,17 @@ export interface PostCommentResponse {
99
status: string;
1010
}
1111

12-
export async function postComment(params: PostCommentRequest): Promise<PostCommentResponse> {
13-
const response = await axiosInstance.post('/comment', params);
12+
export async function postComment(
13+
params: PostCommentRequest,
14+
): Promise<PostCommentResponse | undefined> {
15+
try {
16+
console.log('params : ', params);
17+
console.log('보내는 JSON : ', JSON.stringify(params));
1418

15-
return {
16-
status: response.data,
17-
};
19+
const response = await axiosInstance.post('/comment', params);
20+
return { status: response.data };
21+
} catch (err) {
22+
console.error('❌ postComment 에러:', err);
23+
return undefined;
24+
}
1825
}

react-app/src/components/Comment/Comment.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import Profile from '../Profile/Profile';
22

33
interface CommentProps {
4-
nickname: string | null;
5-
comment: string | null;
4+
nickname: string;
5+
comment: string;
66
}
77

88
const CommentCard = ({ nickname, comment }: CommentProps) => {

react-app/src/components/Comment/CommentInputField.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
1+
import { useRef } from 'react';
2+
13
interface CommentInputFieldProps {
24
nickname: string; // 사용자 이미지 파일명
35
comment: string;
46
onChangeComment: (e: React.ChangeEvent<HTMLInputElement>) => void;
7+
onSubmit: () => Promise<void>;
58
}
69

7-
const CommentInputField = ({ nickname, comment, onChangeComment }: CommentInputFieldProps) => {
10+
const CommentInputField = ({
11+
nickname,
12+
comment,
13+
onChangeComment,
14+
onSubmit,
15+
}: CommentInputFieldProps) => {
16+
const inputRef = useRef<HTMLInputElement>(null);
17+
18+
const handleSubmit = async () => {
19+
if (!comment.trim()) {
20+
inputRef.current?.focus();
21+
return;
22+
}
23+
24+
await onSubmit(); // 상위에서 전달된 async 함수 호출
25+
};
26+
827
return (
928
<div className="flex items-start gap-2 w-full ">
1029
{/* 사용자 이미지 */}
@@ -17,18 +36,22 @@ const CommentInputField = ({ nickname, comment, onChangeComment }: CommentInputF
1736
<div className="flex flex-1 items-center gap-2">
1837
{/* 댓글 입력창 */}
1938
<input
39+
ref={inputRef}
2040
className="flex-1 px-3 py-2 rounded-md border border-gray-300 bg-gray-100
2141
focus:outline-none focus:ring-2 focus:ring-[var(--color-primary-base)]
2242
focus:border-[var(--color-primary-base)]
2343
text-sm sm:text-base font-pr
2444
max-w-[calc(65%)] sm:max-w-full"
2545
value={comment}
26-
onChange={onChangeComment}
46+
onChange={(e) => {
47+
onChangeComment(e);
48+
}}
2749
placeholder="댓글을 입력하세요."
2850
/>
2951

3052
{/* 확인 버튼 */}
3153
<button
54+
onClick={handleSubmit}
3255
className="w-[60px] h-10 text-[12px] font-pr border rounded-lg bg-[var(--color-primary-base)] text-white
3356
flex items-center justify-center text-sm sm:text-base whitespace-nowrap
3457
focus:outline-none focus:ring-2 focus:ring-[var(--color-primary-base)]"

react-app/src/components/Profile/Profile.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
const BASE_URL = '/assets/images/';
22

33
interface ProfileProps {
4-
nickname: string | null;
4+
nickname: string;
55
}
66

77
const Profile = ({ nickname }: ProfileProps) => {
88
return (
99
<div className="w-[40px] h-[40px] sm:w-[60px] sm:h-[60px] rounded-xl">
1010
<img
1111
className="border-[1px] rounded-4xl border-gray-100"
12-
src={`${BASE_URL}${nickname ? 'animal/' + nickname + '.jpg' : 'default-profile.png'}`}
12+
src={`${BASE_URL}${encodeURIComponent(nickname) ? 'animal/' + nickname + '.jpg' : 'default-profile.png'}`}
1313
/>
1414
</div>
1515
);

react-app/src/pages/Comment.tsx

Lines changed: 131 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,16 @@ import CommentCard from '@/components/Comment/Comment';
1111
import CharacterCard from '@/components/Character/Character';
1212
import useIsMobile from '@/hook/useIsMobile';
1313
import CommentInputField from '@/components/Comment/CommentInputField';
14+
import Loading from '@/components/Loading/Loading';
15+
import { postComment } from '@/apis/comment/postComment';
16+
import { useRef } from 'react';
17+
import { useSelector } from 'react-redux';
18+
import { RootState } from '@/store';
1419

1520
const Comment = () => {
21+
// 사용자 정보
22+
const user = useSelector((state: RootState) => state.auth.user)!;
23+
1624
const isMobile = useIsMobile();
1725

1826
// 결과 조회
@@ -24,13 +32,20 @@ const Comment = () => {
2432

2533
// 댓글 입력창 상태 추가
2634
const [commentText, setCommentText] = useState('');
35+
const [isLoading, setIsLoading] = useState(true);
36+
37+
// 댓글 맨 아래로 내리기
38+
const commentsEndRef = useRef<HTMLDivElement>(null);
39+
40+
// 댓글 렌더링, 모바일 여부는 그대로 유지 가능
41+
const displayedComments = isMobile ? comments.slice(-6) : comments;
2742

2843
// 최신 pollIds 불러오기
2944
useEffect(() => {
3045
const fetchAllResults = async () => {
3146
try {
3247
const res = await getLatestPollIds();
33-
console.log('댓글', res);
48+
console.log('최신 투표 id', res);
3449
setPollIds(res);
3550
} catch (error) {
3651
console.error('투표 결과 불러오기 실패 : ', error);
@@ -44,11 +59,13 @@ const Comment = () => {
4459
const fetchResultsByPollIds = async () => {
4560
try {
4661
if (pollIds.length === 0) return;
47-
62+
setIsLoading(true); // 로딩 시작
4863
const res = await Promise.all(pollIds.map((pollId) => getVoteResultByPollId({ pollId })));
4964
setResults(res);
5065
} catch (error) {
5166
console.log('투표 결과 불러오기 실패 : ', error);
67+
} finally {
68+
setIsLoading(false); // 로딩 끝
5269
}
5370
};
5471

@@ -60,41 +77,95 @@ const Comment = () => {
6077
const getAllComments = async () => {
6178
try {
6279
const res = await getComments();
63-
if (res.length === 0) {
64-
}
6580
setComments(res);
6681
} catch (error) {
6782
console.log('댓글 불러오기 실패 : ', error);
6883
}
6984
};
7085
getAllComments();
71-
}, comments);
86+
}, []);
87+
88+
// 댓글 등록 후 스크롤 이동 호출
89+
const scrollToBottom = () => {
90+
commentsEndRef.current?.scrollIntoView({ behavior: 'auto' });
91+
};
92+
93+
// 댓글 목록이 바뀔 때마다 스크롤을 아래로
94+
useEffect(() => {
95+
if (comments.length > 0) {
96+
requestAnimationFrame(() => {
97+
commentsEndRef.current?.scrollIntoView({ behavior: 'auto' });
98+
});
99+
}
100+
}, [comments]);
101+
102+
// 리사이즈 이벤트 감지 후 스크롤 내리기
103+
useEffect(() => {
104+
const handleResize = () => {
105+
// 댓글이 존재하면 맨 아래로 스크롤
106+
if (comments.length > 0) {
107+
commentsEndRef.current?.scrollIntoView({ behavior: 'auto' });
108+
}
109+
};
110+
111+
window.addEventListener('resize', handleResize);
112+
return () => {
113+
window.removeEventListener('resize', handleResize);
114+
};
115+
}, [comments]);
116+
117+
// 댓글 등록
118+
const handleSubmitComment = async () => {
119+
if (!commentText.trim()) {
120+
alert('내용을 입력해주세요');
121+
return;
122+
}
123+
try {
124+
console.log('댓글 내용 : ', commentText);
125+
await postComment({
126+
user_id: user.userId,
127+
comment_text: commentText,
128+
});
129+
130+
const updatedComments = await getComments();
131+
setComments(updatedComments);
132+
setCommentText('');
133+
scrollToBottom();
134+
} catch (error) {
135+
console.log('댓글 등록 실패', error);
136+
}
137+
};
72138

73139
return (
74-
<>
75-
<div className="py-10 sm:p-5 flex flex-col justify-center items-center">
76-
{/* 제목 */}
77-
<div className="flex justify-center items-center gap-3 sm:gap-7 sm:pt-0 pt-5">
78-
{/* 왼쪽 이미지: 모바일에서만 보이게 */}
79-
<img
80-
src="/assets/images/popper-right.png"
81-
className="w-[40px] sm:w-[100px]"
82-
alt="popper-left"
83-
/>
84-
<h1 className="text-2xl font-pm sm:text-3xl">투표 결과</h1>
85-
{/* 오른쪽 이미지: 모든 화면에서 보이지만 크기는 반응형 */}
86-
<img
87-
src="/assets/images/popper-left.png"
88-
className="w-[40px] sm:hidden"
89-
alt="popper-right"
90-
/>
91-
</div>
140+
<div className="pt-10 sm:p-5 flex flex-col justify-center items-center">
141+
{/* 제목 */}
142+
<div className="flex justify-center items-center gap-3 sm:gap-7 sm:pt-0 pt-5">
143+
<img
144+
src="/assets/images/popper-right.png"
145+
className="w-[40px] sm:w-[100px]"
146+
alt="popper-left"
147+
/>
148+
<h1 className="text-2xl font-pm sm:text-3xl">투표 결과</h1>
149+
<img
150+
src="/assets/images/popper-left.png"
151+
className="w-[40px] sm:hidden"
152+
alt="popper-right"
153+
/>
154+
</div>
92155

93-
<div className="p-5 flex flex-col sm:flex-row sm:p-3 gap-10 sm:gap-30">
94-
{/* 카드 영역 */}
95-
<div className="w-full sm:w-auto flex justify-center">
96-
<div className="grid grid-cols-2 grid-rows-2 gap-6 sm:gap-x-10 sm:gap-y-0">
97-
{results.map((poll, idx) => (
156+
<div className="px-5 py-10 flex flex-col sm:flex-row sm:p-3 gap-10 sm:gap-30">
157+
{/* 카드 영역 */}
158+
<div className="w-full sm:w-auto flex justify-center relative">
159+
{/* 로딩 스피너 */}
160+
{isLoading && (
161+
<div className="sm:w-[400px] absolute inset-0 flex justify-center items-center bg-white/80 z-10">
162+
<Loading />
163+
</div>
164+
)}
165+
166+
<div className="grid grid-cols-2 grid-rows-2 gap-6 sm:gap-x-10 sm:gap-y-0">
167+
{!isLoading &&
168+
results.map((poll, idx) => (
98169
<div
99170
key={poll.pollId}
100171
className={`w-full ${!isMobile && idx % 2 === 1 ? 'mt-7' : ''}`}
@@ -107,43 +178,44 @@ const Comment = () => {
107178
/>
108179
</div>
109180
))}
110-
</div>
111181
</div>
182+
</div>
112183

113-
{/* 댓글 */}
114-
<div className="flex-1 w-full h-[550px] border border-gray-300 rounded-2xl flex flex-col justify-between px-4 py-2">
115-
{/* 스크롤 가능한 댓글 리스트 */}
116-
<div className="flex-1 overflow-y-auto pr-2">
117-
{comments.length === 0 ? (
118-
// flex items-center justify-center h-full w-full
119-
<div className="pl-10 w-full flex items-center justify-center h-full">
120-
<CharacterCard />
121-
</div>
122-
) : (
123-
<div className="flex flex-col items-center gap-2 w-full">
124-
{comments.map((comment, idx) => (
125-
<CommentCard
126-
key={idx}
127-
nickname={comment.random_nickname}
128-
comment={comment.comment_text}
129-
/>
130-
))}
131-
</div>
132-
)}
133-
</div>
184+
{/* 댓글 영역 */}
185+
<div className=" flex-1 w-full h-[550px] border border-gray-300 rounded-2xl flex flex-col justify-between px-4 py-2">
186+
{/* 스크롤 가능한 댓글 리스트 */}
187+
<div className="flex-1 overflow-y-auto pr-2 max-h-[400px] sm:max-h-full">
188+
{comments.length === 0 ? (
189+
<div className="pl-10 w-full flex items-center justify-center h-full">
190+
<CharacterCard />
191+
</div>
192+
) : (
193+
<div className="flex flex-col items-center gap-2 w-full">
194+
{displayedComments.map((comment, idx) => (
195+
<CommentCard
196+
key={idx}
197+
nickname={comment.random_nickname}
198+
comment={comment.comment_text}
199+
/>
200+
))}
134201

135-
{/* 고정된 입력창 */}
136-
<div className="mt-4">
137-
<CommentInputField
138-
nickname="부드럽게 움직이는 황금도토리"
139-
comment={commentText}
140-
onChangeComment={(e) => setCommentText(e.target.value)}
141-
/>
142-
</div>
202+
<div ref={commentsEndRef} />
203+
</div>
204+
)}
205+
</div>
206+
207+
{/* 고정된 입력창 */}
208+
<div className="mt-4">
209+
<CommentInputField
210+
nickname={user.randomNickname}
211+
comment={commentText}
212+
onChangeComment={(e) => setCommentText(e.target.value)}
213+
onSubmit={handleSubmitComment} // 이 부분 추가
214+
/>
143215
</div>
144216
</div>
145217
</div>
146-
</>
218+
</div>
147219
);
148220
};
149221

react-app/src/store/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import toastReducer from './slices/toastSlice';
1919
const persistConfig = {
2020
key: 'root',
2121
storage,
22-
whitelist: [],
22+
whitelist: ['auth'],
2323
};
2424

2525
// 모든 슬라이스 리듀서를 통합

springboot-app/src/main/java/com/uplus/eureka/comment/controller/CommentController.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,19 @@ public ResponseEntity<Comment> getCommentById(@PathVariable("commentId") Integer
5959
})
6060
@PostMapping
6161
public ResponseEntity<String> insertComment(@RequestBody CommentRequest comment) {
62+
System.out.println("------------------------------CONTROLLER-------------------------------------");
6263
System.out.println(comment);
63-
if(comment.getUserId() == null){
64-
return new ResponseEntity<String>("FAILED", HttpStatus.NOT_FOUND);
64+
try {
65+
if(comment.getUserId() == null){
66+
return new ResponseEntity<String>("FAILED", HttpStatus.NOT_FOUND);
67+
}
68+
commentService.insertComment(comment);
69+
return new ResponseEntity<String>("SUCCESS", HttpStatus.CREATED);
70+
71+
} catch (Exception e) {
72+
e.printStackTrace();
73+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("처리 중 오류 발생");
6574
}
66-
commentService.insertComment(comment);
67-
return new ResponseEntity<String>("SUCCESS", HttpStatus.CREATED);
6875
}
6976

7077
@Operation(summary = "댓글 수정", description = "본인이 작성한 댓글 수정")

0 commit comments

Comments
 (0)