Skip to content

Commit 73d462b

Browse files
authored
Merge pull request #62 from codeit-maso/feature/Yun
✨ feat: /post/id 페이지 구현 - 작성한 메시지 카드로 보여주기, 무한스크롤 구현
2 parents 5dcf4e3 + 0b2cf70 commit 73d462b

File tree

11 files changed

+194
-30
lines changed

11 files changed

+194
-30
lines changed

package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
},
1414
"dependencies": {
1515
"axios": "^1.5.0",
16+
"dompurify": "^3.2.5",
1617
"react": "^18.2.0",
1718
"react-dom": "^18.2.0",
1819
"react-quill": "^2.0.0",

src/api/getMessages.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { api } from './api';
2+
3+
export default async function getMessages(id, offset, limit) {
4+
try {
5+
const teamId = import.meta.env.VITE_TEAM_ID;
6+
const res = await api.get(
7+
`/${teamId}/recipients/${id}/messages/?limit=${limit}&offset=${offset}`,
8+
);
9+
return res.data;
10+
} catch {
11+
return null;
12+
}
13+
}

src/api/getRecipient.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { api } from './api';
2+
3+
export default async function getRecipient(id) {
4+
try {
5+
const teamId = import.meta.env.VITE_TEAM_ID;
6+
const res = await api.get(`/${teamId}/recipients/${id}/`);
7+
return res.data;
8+
} catch {
9+
return null;
10+
}
11+
}

src/components/Badge/Badge.jsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import styles from './Badge.module.scss';
22

33
const relationship = {
4-
acquaintance: '지인',
5-
colleague: '동료',
6-
family: '가족',
7-
friend: '친구',
4+
지인: 'acquaintance',
5+
동료: 'colleague',
6+
가족: 'family',
7+
친구: 'friend',
88
};
99

1010
export default function Badge({ relation }) {
1111
return (
12-
<div className={`${styles.badge} ${styles[relation]}`}>
13-
{relationship[relation]}
12+
<div className={`${styles.badge} ${styles[relationship[relation]]}`}>
13+
{relation}
1414
</div>
1515
);
1616
}

src/components/Card/Card.jsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
1+
import DOMPurify from 'dompurify';
2+
import { useNavigate } from 'react-router-dom';
13
import deleteIcon from '../../assets/images/delete.svg';
24
import plus from '../../assets/images/plus.svg';
35
import Badge from '../Badge/Badge';
46
import styles from './Card.module.scss';
57

68
export default function Card({
79
image,
10+
recipientId,
811
sender,
912
relationship,
10-
content,
13+
children,
1114
createdAt,
1215
empty = false,
1316
}) {
17+
const navigate = useNavigate();
18+
const sanitizedHTML = DOMPurify.sanitize(children);
19+
20+
function handleClick() {
21+
navigate(`/post/${recipientId}/message/`);
22+
}
23+
1424
return (
1525
<article className={`${styles.card} ${empty ? styles['card--empty'] : ''}`}>
1626
{empty ? (
17-
<div>
27+
<div onClick={handleClick}>
1828
<img src={plus} alt="추가하기" />
1929
</div>
2030
) : (
@@ -41,7 +51,7 @@ export default function Card({
4151
</header>
4252
<div className={styles['card__body']}>
4353
<div className={styles['card__content']}>
44-
<p>{content}</p>
54+
<div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />
4555
</div>
4656
</div>
4757
<footer className={styles['card__footer']}>{createdAt}</footer>

src/components/Card/Card.module.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
img {
2020
width: 56px;
2121
height: 56px;
22+
cursor: pointer;
2223
}
2324
}
2425

@@ -52,6 +53,9 @@
5253
gap: 6px;
5354

5455
.card__profile-name {
56+
overflow: hidden;
57+
text-overflow: ellipsis;
58+
white-space: nowrap;
5559
@include font-20-regular;
5660

5761
span {

src/pages/Home/Home.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default function Home() {
4242
</section>
4343

4444
<div className={styles.buttonWrapper}>
45-
<Button text="구경해보기" onClick={() => navigate()} />
45+
<Button onClick={() => navigate('/list')}>구경해보기</Button>
4646
</div>
4747
</div>
4848
);

src/pages/MessageForm/MessageForm.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export default function MessageForm() {
6969

7070
localStorage.removeItem('quill-content');
7171
setMessage('');
72-
navigate(`/post/${res.id}`);
72+
navigate(`/post/${id}`);
7373
} catch (error) {
7474
console.error('메세지 전송 실패', error);
7575
}

src/pages/Recipient/Recipient.jsx

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,105 @@
1-
import { useState, useEffect } from 'react';
2-
import { useParams } from 'react-router-dom';
1+
import { useParams } from 'react-router';
2+
import { useEffect, useState, useRef } from 'react';
3+
import getRecipient from '../../api/getRecipient';
4+
import getMessages from '../../api/getMessages';
35
import HeaderService from '../../components/recipient/HeaderService/HeaderService';
4-
import fetchRecipient from '../../api/fetchRecipient';
6+
import Card from '../../components/Card/Card';
7+
import styles from './Recipient.module.scss';
58

69
export default function Recipient() {
7-
const [recipient, setRecipient] = useState(null);
810
const { id } = useParams();
11+
const [postData, setPostData] = useState(null);
12+
const [messages, setMessages] = useState([]);
13+
const [offset, setOffset] = useState(0);
14+
const [loading, setLoading] = useState(false);
15+
const [hasNextMessage, setHasNextMessage] = useState(false);
16+
const observerRef = useRef();
917

1018
useEffect(() => {
11-
if (!id) {
12-
console.error('Recipient ID가 제공되지 않았습니다');
13-
return;
14-
}
15-
16-
async function loadData() {
19+
const fetchRecipient = async () => {
1720
try {
18-
const recipientData = await fetchRecipient(id);
19-
20-
setRecipient(recipientData);
21+
const recipient = await getRecipient(id);
22+
setPostData(recipient);
23+
setHasNextMessage(recipient.messageCount > 0);
24+
setLoading(false);
2125
} catch (error) {
22-
console.error('데이터 가져오기 실패:', error);
26+
console.error('데이터 로딩 실패:', error.response.data);
2327
}
24-
}
28+
};
2529

26-
loadData();
30+
fetchRecipient();
2731
}, [id]);
2832

33+
useEffect(() => {
34+
setLoading(true);
35+
const fetchMessages = async () => {
36+
try {
37+
const limit = offset === 0 ? 8 : 9;
38+
const newMessages = await getMessages(id, offset, limit);
39+
offset === 0
40+
? setMessages(newMessages.results)
41+
: setMessages((prev) => [...prev, ...newMessages.results]);
42+
if (!postData) return;
43+
setHasNextMessage(offset < postData.messageCount);
44+
setLoading(false);
45+
} catch (error) {
46+
console.error(
47+
'데이터 로딩 실패:',
48+
error.response?.data || error.message,
49+
);
50+
}
51+
};
52+
53+
fetchMessages();
54+
}, [id, offset]);
55+
56+
useEffect(() => {
57+
const observer = new IntersectionObserver((entries) => {
58+
const firstEntry = entries[0];
59+
if (firstEntry.isIntersecting && hasNextMessage && !loading) {
60+
loadMoreMessages();
61+
}
62+
});
63+
if (observerRef.current) observer.observe(observerRef.current);
64+
return () => {
65+
if (observerRef.current) observer.unobserve(observerRef.current);
66+
};
67+
}, [observerRef.current, hasNextMessage, loading]);
68+
69+
const loadMoreMessages = () => {
70+
const limit = offset === 0 ? 8 : 9;
71+
setOffset((prev) => prev + limit);
72+
};
73+
74+
if (!postData || messages.length < 0) return <div>Loading...</div>;
75+
2976
return (
30-
<div>
31-
<HeaderService recipient={recipient} />
32-
</div>
77+
<>
78+
<HeaderService />
79+
<div
80+
className={`${styles['post-container']} ${!postData.backgroundImageURL ? styles[`background--${postData.backgroundColor}`] : ''}`}
81+
style={
82+
postData.backgroundImageURL
83+
? { backgroundImage: `url(${postData.backgroundImageURL})` }
84+
: {}
85+
}
86+
>
87+
<div className={styles['card-container']}>
88+
<Card recipientId={id} empty={true} />
89+
{messages.map((msg) => (
90+
<Card
91+
key={msg.id}
92+
image={msg.profileImageURL}
93+
sender={msg.sender}
94+
relationship={msg.relationship}
95+
createdAt={msg.createdAt.slice(0, 10).split('-').join('.')}
96+
>
97+
{msg.content}
98+
</Card>
99+
))}
100+
</div>
101+
{hasNextMessage && <div ref={observerRef} className="load"></div>}
102+
</div>
103+
</>
33104
);
34105
}

0 commit comments

Comments
 (0)