Skip to content

Commit 7df452e

Browse files
committed
댓글 기능 구현
1 parent 4408dbc commit 7df452e

File tree

7 files changed

+239
-13
lines changed

7 files changed

+239
-13
lines changed

polling-app-client/package-lock.json

Lines changed: 30 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

polling-app-client/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
"name": "polling-app-client",
33
"version": "0.1.0",
44
"private": true,
5-
"proxy": "http://localhost:8080",
6-
5+
"proxy": "http://localhost:8080",
76
"dependencies": {
87
"antd": "^3.2.2",
98
"react": "^16.5.2",
109
"react-dom": "^16.5.2",
1110
"react-router-dom": "^4.3.1",
12-
"react-scripts": "1.1.5"
11+
"react-scripts": "1.1.5",
12+
"slick-carousel": "^1.8.1"
1313
},
1414
"scripts": {
1515
"start": "react-app-rewired start",

polling-app-client/src/app/App.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import HomeDashboard from '../components/Dashboard/HomeDashboard';
22
import GroupPollList from '../components/Dashboard/GroupPollList';
3+
import MyComments from '../components/Dashboard/MyComments';
4+
35
import React, { Component } from 'react';
46
import './App.css';
57
import {
@@ -20,6 +22,7 @@ import NotFound from '../common/NotFound';
2022
import LoadingIndicator from '../common/LoadingIndicator';
2123
import PrivateRoute from '../common/PrivateRoute';
2224

25+
2326
import { Layout, notification } from 'antd';
2427
const { Content } = Layout;
2528

@@ -122,12 +125,19 @@ class App extends Component {
122125
</Route>
123126
<PrivateRoute authenticated={this.state.isAuthenticated} path="/poll/new" component={NewPoll} handleLogout={this.handleLogout}></PrivateRoute>
124127
<PrivateRoute
125-
path="/groups/:groupId/polls"
126-
component={GroupPollList}
127-
authenticated={this.state.isAuthenticated}
128-
/> <Route component={NotFound}></Route>
129-
130-
</Switch>
128+
path="/groups/:groupId/polls"
129+
component={GroupPollList}
130+
authenticated={this.state.isAuthenticated}
131+
/>
132+
133+
<PrivateRoute
134+
path="/polls/:pollId/comments"
135+
component={MyComments}
136+
authenticated={this.state.isAuthenticated}
137+
/>
138+
139+
<Route component={NotFound} />
140+
</Switch>
131141
</div>
132142
</Content>
133143
</Layout>
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import React, { Component } from 'react';
2+
3+
class MyComments extends Component {
4+
state = {
5+
comments: [],
6+
newComment: '',
7+
loading: false,
8+
error: null,
9+
};
10+
11+
componentDidMount() {
12+
this.loadComments();
13+
}
14+
15+
getJwtToken = () => localStorage.getItem('accessToken');
16+
17+
loadComments = () => {
18+
const pollId = this.props.match.params.pollId;
19+
const token = this.getJwtToken();
20+
21+
this.setState({ loading: true, error: null });
22+
23+
fetch(`/api/polls/${pollId}/comments`, {
24+
headers: { Authorization: `Bearer ${token}` },
25+
})
26+
.then(res => {
27+
if (!res.ok) throw new Error('댓글 목록을 불러올 수 없습니다.');
28+
return res.json();
29+
})
30+
.then(data => {
31+
this.setState({
32+
comments: Array.isArray(data.content) ? data.content : [],
33+
loading: false,
34+
});
35+
})
36+
.catch(error => this.setState({ error: error.message, loading: false }));
37+
};
38+
39+
handleInputChange = e => {
40+
this.setState({ newComment: e.target.value });
41+
};
42+
43+
handleSubmit = e => {
44+
e.preventDefault();
45+
const pollId = this.props.match.params.pollId;
46+
const { newComment } = this.state;
47+
const token = this.getJwtToken();
48+
49+
if (!newComment.trim()) return;
50+
51+
fetch(`/api/polls/${pollId}/comments`, {
52+
method: 'POST',
53+
headers: {
54+
'Content-Type': 'application/json',
55+
Authorization: `Bearer ${token}`,
56+
},
57+
body: JSON.stringify({ content: newComment }),
58+
})
59+
.then(res => {
60+
if (!res.ok) throw new Error('댓글 작성에 실패했습니다.');
61+
return res.json();
62+
})
63+
.then(() => {
64+
this.setState({ newComment: '' });
65+
this.loadComments();
66+
})
67+
.catch(error => this.setState({ error: error.message }));
68+
};
69+
70+
handleDelete = commentId => {
71+
const token = this.getJwtToken();
72+
73+
fetch(`/api/comments/${commentId}`, {
74+
method: 'DELETE',
75+
headers: { Authorization: `Bearer ${token}` },
76+
})
77+
.then(res => {
78+
if (!res.ok) throw new Error('댓글 삭제에 실패했습니다.');
79+
return res.json();
80+
})
81+
.then(() => this.loadComments())
82+
.catch(error => this.setState({ error: error.message }));
83+
};
84+
85+
render() {
86+
const { comments, newComment, loading, error } = this.state;
87+
88+
return (
89+
<div style={{ maxWidth: 600, margin: 'auto', fontFamily: 'Arial, sans-serif' }}>
90+
<h3 style={{ borderBottom: '2px solid #1890ff', paddingBottom: 8 }}>댓글</h3>
91+
92+
{error && (
93+
<p style={{ color: 'red', backgroundColor: '#fff1f0', padding: 10, borderRadius: 4 }}>
94+
{error}
95+
</p>
96+
)}
97+
98+
{loading ? (
99+
<p>로딩 중...</p>
100+
) : (
101+
<ul style={{ listStyle: 'none', padding: 0 }}>
102+
{comments.map(c => (
103+
<li
104+
key={c.id}
105+
style={{
106+
borderBottom: '1px solid #f0f0f0',
107+
padding: '12px 0',
108+
display: 'flex',
109+
justifyContent: 'space-between',
110+
alignItems: 'center',
111+
}}
112+
>
113+
<div>
114+
<div style={{ fontWeight: 'bold', color: '#1890ff' }}>
115+
{c.username || `User${c.userId || ''}`}
116+
</div>
117+
<div style={{ fontSize: 14, color: '#555', margin: '4px 0' }}>{c.content}</div>
118+
<div style={{ fontSize: 12, color: '#999' }}>{c.createdAt}</div>
119+
</div>
120+
<button
121+
onClick={() => this.handleDelete(c.id)}
122+
style={{
123+
border: 'none',
124+
backgroundColor: 'transparent',
125+
color: '#ff4d4f',
126+
cursor: 'pointer',
127+
fontSize: 14,
128+
}}
129+
title="댓글 삭제"
130+
>
131+
삭제
132+
</button>
133+
</li>
134+
))}
135+
</ul>
136+
)}
137+
138+
<form onSubmit={this.handleSubmit} style={{ marginTop: 20 }}>
139+
<textarea
140+
value={newComment}
141+
onChange={this.handleInputChange}
142+
placeholder="댓글을 입력하세요"
143+
rows={4}
144+
style={{
145+
width: '100%',
146+
padding: 10,
147+
fontSize: 14,
148+
borderRadius: 4,
149+
border: '1px solid #d9d9d9',
150+
resize: 'vertical',
151+
}}
152+
/>
153+
<button
154+
type="submit"
155+
disabled={!newComment.trim()}
156+
style={{
157+
marginTop: 8,
158+
padding: '8px 16px',
159+
fontSize: 14,
160+
backgroundColor: '#1890ff',
161+
color: 'white',
162+
border: 'none',
163+
borderRadius: 4,
164+
cursor: newComment.trim() ? 'pointer' : 'not-allowed',
165+
}}
166+
>
167+
댓글 작성
168+
</button>
169+
</form>
170+
</div>
171+
);
172+
}
173+
}
174+
175+
export default MyComments;

polling-app-client/src/poll/Poll.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ class Poll extends Component {
118118
this.getTimeRemaining(this.props.poll)
119119
}
120120
</span>
121+
<Link to={`/polls/${this.props.poll.id}/comments`} style={{ marginLeft: 20 }}>
122+
<Button type="link">댓글 보기 / 작성하기</Button>
123+
</Link>
121124
</div>
122125
</div>
123126
);

polling-app-client/yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4730,6 +4730,11 @@ [email protected]:
47304730
dependencies:
47314731
jest-cli "^20.0.4"
47324732

4733+
jquery@>=1.8.0:
4734+
version "3.7.1"
4735+
resolved "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz"
4736+
integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==
4737+
47334738
js-base64@^2.1.9:
47344739
version "2.4.9"
47354740
resolved "https://registry.npmjs.org/js-base64/-/js-base64-2.4.9.tgz"
@@ -7813,6 +7818,11 @@ [email protected]:
78137818
dependencies:
78147819
is-fullwidth-code-point "^2.0.0"
78157820

7821+
slick-carousel@^1.8.1:
7822+
version "1.8.1"
7823+
resolved "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz"
7824+
integrity sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==
7825+
78167826
snapdragon-node@^2.0.1:
78177827
version "2.1.1"
78187828
resolved "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz"

polling-app-server/src/main/resources/application.properties

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ server.compression.enabled=true
44

55
## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
66
spring.datasource.url= jdbc:mysql://localhost:3306/polling_app?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false
7-
spring.datasource.username= root
8-
spring.datasource.password= asd798852!
9-
7+
spring.datasource.username= sumin
8+
spring.datasource.password= rlatnals
109

1110
## Hibernate Properties
1211
# The SQL dialect makes Hibernate generate better SQL for the chosen database

0 commit comments

Comments
 (0)