Skip to content

Commit 5dcf4e3

Browse files
authored
✨ feat: 폰트 드롭다운 구현 및 메세지 페이지 구현
✨ feat: 폰트 드롭다운 구현 및 메세지 페이지 구현
2 parents c90b76e + 0a8a189 commit 5dcf4e3

File tree

7 files changed

+341
-3
lines changed

7 files changed

+341
-3
lines changed

index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,24 @@
33
<head>
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<link
7+
href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap"
8+
rel="stylesheet"
9+
/>
610
<link
711
rel="stylesheet"
812
as="style"
913
crossorigin
1014
href="https://cdn.jsdelivr.net/gh/orioncactus/[email protected]/dist/web/static/pretendard.min.css"
1115
/>
16+
<link
17+
href="https://hangeul.pstatic.net/hangeul_static/css/nanum-myeongjo.css"
18+
rel="stylesheet"
19+
/>
20+
<link
21+
href="https://hangeul.pstatic.net/hangeul_static/css/NanumSonPyeonJiCe.css"
22+
rel="stylesheet"
23+
/>
1224
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
1325
<title>너의 마음을 전달해줘 - Rolling</title>
1426
</head>

src/api/createMessage.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { api } from './api';
2+
3+
export default async function createMessage({
4+
team,
5+
recipientId,
6+
sender,
7+
profileImageURL,
8+
relationship,
9+
content,
10+
font,
11+
}) {
12+
const res = await api.post(`/${team}/recipients/${recipientId}/messages/`, {
13+
team,
14+
recipientId,
15+
sender,
16+
profileImageURL,
17+
relationship,
18+
content,
19+
font,
20+
});
21+
22+
return res.data;
23+
}

src/components/Editor/Editor.jsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo } from 'react';
1+
import { useMemo, useEffect } from 'react';
22
import ReactQuill from 'react-quill';
33
import 'react-quill/dist/quill.snow.css';
44
import styled from './Editor.module.scss';
@@ -14,7 +14,7 @@ const formats = [
1414
'background',
1515
];
1616

17-
export default function Editor({ value, onChange }) {
17+
export default function Editor({ value, onChange, font }) {
1818
const modules = useMemo(() => {
1919
return {
2020
toolbar: [
@@ -27,9 +27,18 @@ export default function Editor({ value, onChange }) {
2727
};
2828
}, []);
2929

30+
useEffect(() => {
31+
const editorElement = document.querySelector('.ql-editor');
32+
if (editorElement) {
33+
editorElement.style.fontFamily = font;
34+
editorElement.style.fontSize = '18px';
35+
}
36+
}, [font]);
37+
3038
return (
3139
<div className={styled['editor']}>
3240
<h2 className={styled['editor__title']}>내용을 입력해 주세요</h2>
41+
3342
<div className={styled['editor__box']}>
3443
<ReactQuill
3544
theme="snow"
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { useState, useRef, useId } from 'react';
2+
import useDetectClose from '../../hooks/useDetectClose';
3+
import styles from './FontSelect.module.scss';
4+
5+
const FONTS = ['Noto Sans', 'Pretendard', 'NanumMyeongjo', 'NanumSonPyeonjiCe'];
6+
7+
const fontClassMap = {
8+
NotoSans: styles['font-noto'],
9+
Pretendard: styles['font-pretendard'],
10+
NanumMyeongjo: styles['font-nanum-myeongjo'],
11+
NanumSonPyeonjiCe: styles['font-naunm-hand'],
12+
};
13+
14+
const formatFontName = (font) =>
15+
font
16+
.replace('NanumMyeongjo', '나눔명조')
17+
.replace('NanumSonPyeonjiCe', '나눔손글씨 손편지체');
18+
19+
export default function FontSelect({ defaultValue = 'Noto Sans', onChange }) {
20+
const dropdownRef = useRef(null);
21+
const id = useId();
22+
const [selected, setSelected] = useState(defaultValue);
23+
const [isOpen, setIsOpen] = useDetectClose(dropdownRef);
24+
25+
const handleSelect = (font) => {
26+
setSelected(font);
27+
onChange?.(font);
28+
setIsOpen(false);
29+
};
30+
31+
return (
32+
<div className={styles['dropdown']}>
33+
<label htmlFor={id} className={styles['dropdown__label']}>
34+
폰트 선택
35+
</label>
36+
<div ref={dropdownRef} className={styles['dropdown-body']}>
37+
<button
38+
id={id}
39+
type="button"
40+
className={`${styles['dropdown__button']} ${fontClassMap[selected]}`}
41+
onClick={() => setIsOpen((prev) => !prev)}
42+
>
43+
{formatFontName(selected)}
44+
<span
45+
className={`${styles.dropdown__arrow} ${isOpen ? styles.open : ''}`}
46+
/>
47+
</button>
48+
{isOpen && (
49+
<ul className={styles['dropdown__list']}>
50+
{FONTS.map((font) => (
51+
<li
52+
key={font}
53+
className={`${styles['dropdown__item']} ${fontClassMap[font]}`}
54+
onClick={() => handleSelect(font)}
55+
>
56+
{formatFontName(font)}
57+
</li>
58+
))}
59+
</ul>
60+
)}
61+
</div>
62+
</div>
63+
);
64+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
@use '../../assets/styles/variables.scss' as *;
2+
3+
.dropdown {
4+
display: flex;
5+
flex-direction: column;
6+
width: 320px;
7+
height: 98px;
8+
gap: 12px;
9+
10+
&__label {
11+
@include font-24-bold;
12+
color: $gray-900;
13+
}
14+
15+
&__button {
16+
all: unset;
17+
display: flex;
18+
justify-content: space-between;
19+
align-items: center;
20+
width: 286px;
21+
height: 24px;
22+
padding: 12px 16px;
23+
border: 1px solid $gray-300;
24+
border-radius: 8px;
25+
@include font-16-regular;
26+
color: $gray-500;
27+
28+
&:active {
29+
border: 2px solid $gray-500;
30+
color: $gray-900;
31+
}
32+
33+
&:hover {
34+
border: 1px solid $gray-500;
35+
}
36+
37+
&:focus {
38+
border: 2px solid $gray-500;
39+
color: $gray-900;
40+
}
41+
}
42+
43+
&__arrow {
44+
display: inline-block;
45+
width: 16px;
46+
height: 16px;
47+
background-image: url('/src/assets/images/arrow.svg');
48+
background-size: contain;
49+
background-repeat: no-repeat;
50+
}
51+
52+
&__arrow.open {
53+
transform: rotate(180deg);
54+
}
55+
56+
&__list {
57+
display: flex;
58+
justify-content: space-between;
59+
flex-direction: column;
60+
width: 318px;
61+
height: 220px;
62+
margin-top: 8px;
63+
padding: 10px 1px;
64+
border: 1px solid $gray-300;
65+
border-radius: 8px;
66+
background-color: $white;
67+
list-style: none;
68+
}
69+
70+
&__item {
71+
width: 316px;
72+
height: 50px;
73+
padding: 12px 16px;
74+
@include font-16-regular;
75+
color: $gray-900;
76+
77+
&:hover {
78+
background-color: $gray-100;
79+
}
80+
}
81+
}
82+
83+
.font-noto {
84+
font-family: 'Noto Sans', sans-serif;
85+
}
86+
87+
.font-pretendard {
88+
font-family: 'Pretendard', sans-serif;
89+
}
90+
91+
.font-nanum-myeongjo {
92+
font-family: 'Nanum Myeongjo', serif;
93+
}
94+
95+
.font-naunm-hand {
96+
font-family: 'NanumSonPyeonjiCe', cursive;
97+
}
Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,112 @@
1+
import { useState, useEffect } from 'react';
2+
import { useNavigate, useParams } from 'react-router-dom';
3+
import FormInput from '../../components/common/FormInput';
4+
import UserProfileSelector from '../../components/UserProfileSelector/UserProfileSelector';
5+
import DEFAULT_PROFILE_IMAGE from '../../constants/image';
6+
import RelationshipSelect from '../../components/RelationshipSelect/RelationshipSelect';
7+
import Editor from '../../components/Editor/Editor';
8+
import FontSelect from '../../components/FontSelect/FontSelect';
9+
import Button from '../../components/common/Button';
10+
import createMessage from '../../api/createMessage';
11+
import styles from './MessageForm.module.scss';
12+
113
export default function MessageForm() {
2-
return <div style={{ fontSize: '30px' }}>Route test component ^_^ </div>;
14+
const { id } = useParams();
15+
const [sender, setSender] = useState('');
16+
const [isError, setIsError] = useState(false);
17+
const [profileImage, setProfileImage] = useState(DEFAULT_PROFILE_IMAGE);
18+
const [relationship, setRelationship] = useState('지인');
19+
const [message, setMessage] = useState('');
20+
const [font, setFont] = useState('Noto Sans');
21+
22+
const stripHtml = (html) => html.replace(/<[^>]+>/g, '').trim();
23+
const isValid = sender.trim() !== '' && stripHtml(message) !== '';
24+
25+
const navigate = useNavigate();
26+
27+
function handleInputChange(e) {
28+
setSender(e.target.value);
29+
}
30+
31+
function handleBlur() {
32+
setIsError(sender.trim() === '');
33+
}
34+
35+
useEffect(() => {
36+
const saved = localStorage.getItem('quill-content');
37+
if (saved) {
38+
setMessage(saved);
39+
}
40+
}, []);
41+
42+
useEffect(() => {
43+
const timeout = setTimeout(
44+
() => {
45+
localStorage.removeItem('quill-content');
46+
console.log('하루 뒤 자동 삭제');
47+
},
48+
1000 * 60 * 60 * 24,
49+
);
50+
51+
return () => clearTimeout(timeout);
52+
}, []);
53+
54+
useEffect(() => {
55+
localStorage.setItem('quill-content', message);
56+
}, [message]);
57+
58+
async function handleSubmit() {
59+
try {
60+
const res = await createMessage({
61+
team: '15-7',
62+
recipientId: Number(id),
63+
sender,
64+
profileImageURL: profileImage,
65+
relationship,
66+
content: message,
67+
font,
68+
});
69+
70+
localStorage.removeItem('quill-content');
71+
setMessage('');
72+
navigate(`/post/${res.id}`);
73+
} catch (error) {
74+
console.error('메세지 전송 실패', error);
75+
}
76+
}
77+
78+
return (
79+
<div className={styles['message-form']}>
80+
<div className={styles['message-form__content']}>
81+
<div className={styles['message-form__input']}>
82+
<FormInput
83+
label="Form."
84+
placeholder="이름을 입력해 주세요."
85+
value={sender}
86+
onChange={handleInputChange}
87+
onBlur={handleBlur}
88+
isError={isError}
89+
/>
90+
</div>
91+
<div className={styles['message-form__profile-selector']}>
92+
<UserProfileSelector onSelect={setProfileImage} />
93+
</div>
94+
<div className={styles['message-form__relationship-select']}>
95+
<RelationshipSelect
96+
defaultValue={relationship}
97+
onChange={setRelationship}
98+
/>
99+
</div>
100+
<div className={styles['message-form__editor']}>
101+
<Editor value={message} onChange={setMessage} font={font} />
102+
</div>
103+
<div className={styles['message-form__font-select']}>
104+
<FontSelect value={font} onChange={setFont} />
105+
</div>
106+
</div>
107+
<Button type="create" onClick={handleSubmit} disabled={!isValid}>
108+
생성하기
109+
</Button>
110+
</div>
111+
);
3112
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@use '../../assets/styles/variables.scss' as *;
2+
3+
.message-form {
4+
display: flex;
5+
flex-direction: column;
6+
width: 72rem;
7+
margin: 40px auto 36px;
8+
gap: 62px;
9+
10+
&__content {
11+
display: flex;
12+
flex-direction: column;
13+
height: 94.4rem;
14+
gap: 50px;
15+
}
16+
17+
&__relationship-select {
18+
z-index: 1;
19+
}
20+
21+
&__font-select {
22+
z-index: 1;
23+
}
24+
}

0 commit comments

Comments
 (0)