Skip to content

Commit 336e2bb

Browse files
author
jyn
committed
Merge branch 'develop' into feature/jeon
2 parents 5a4bf00 + 8515d7d commit 336e2bb

File tree

4 files changed

+247
-19
lines changed

4 files changed

+247
-19
lines changed

README.md

Lines changed: 204 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,204 @@
1-
# 롤링 프로젝트
1+
# Rolling
2+
마음을 종이비행기에 담아 전하세요. 부담 없이, 따뜻하게.
3+
4+
## 🚀 배포 주소
5+
[롤링 바로가기](https://rolling-gamma.vercel.app/)
6+
7+
## 📝 프로젝트 소개
8+
Rolling은 소중한 사람들에게 마음을 전할 수 있는 롤링 페이퍼 서비스입니다. 이름, 배경, 색삭을 설정하여 롤링 페이퍼를 만들고, 메시지와 이모지 반응을 남겨보세요.
9+
10+
## 📚 기술 스택
11+
### Language
12+
<img src="https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E">
13+
14+
### FrontEnd
15+
<img src="https://img.shields.io/badge/react-61DAFB?style=for-the-badge&logo=react&logoColor=black"><img src="https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white">
16+
17+
### Style
18+
<img src="https://img.shields.io/badge/SCSS-CC6699?style=for-the-badge&logo=sass&logoColor=white"><img src="https://img.shields.io/badge/CSS_Modules-000000?style=for-the-badge&logo=css-modules&logoColor=white">
19+
20+
### 도구 및 유틸리티
21+
<img src="https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white">
22+
23+
### API
24+
<img src="https://img.shields.io/badge/axios-5A29E4?style=for-the-badge&logo=axios&logoColor=white">
25+
26+
### 코드 포매터 및 검사 도구
27+
<img src="https://img.shields.io/badge/ESLint-4B32C3?style=for-the-badge&logo=eslint&logoColor=white"><img src="https://img.shields.io/badge/Prettier-F7B93E?style=for-the-badge&logo=prettier&logoColor=black">
28+
29+
### 협업툴
30+
<img src="https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white"><img src="https://img.shields.io/badge/Notion-%23000000.svg?style=for-the-badge&logo=notion&logoColor=white"><img src="https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white">
31+
32+
### 배포 및 CI/CD
33+
<img src="https://img.shields.io/badge/vercel-%23000000.svg?style=for-the-badge&logo=vercel&logoColor=white"><img src="https://img.shields.io/badge/GitHub_Actions-2088FF?style=for-the-badge&logo=github-actions&logoColor=white">
34+
35+
36+
## 👥 팀원 소개 및 역할 분담
37+
<table width="100%">
38+
<tr align="center" bgcolor="#f8f8f8">
39+
<th width="20%">박광민</th>
40+
<th width="20%">이찬호</th>
41+
<th width="20%">이승민</th>
42+
<th width="20%">전지윤</th>
43+
<th width="20%">윤진우</th>
44+
</tr>
45+
<tr align="center">
46+
<td><img src="https://github.com/user-attachments/assets/167246c7-9218-4208-a6ef-fcac9f3851e0" width="180" height="180"></td>
47+
<td><img src="https://github.com/user-attachments/assets/b7b784fc-30c3-4a1a-8b2f-14f899e1042c" width="180" height="180"></td>
48+
<td><img src="https://github.com/user-attachments/assets/d688f02f-f451-4364-b9a4-326f953d8206" width="180" height="180"></td>
49+
<td><img src="https://github.com/user-attachments/assets/4ea665ec-37f8-4068-aede-b3cbd62642f6" width="180" height="180"></td>
50+
<td><img src="https://github.com/user-attachments/assets/b106db3e-240f-4f58-814c-8c2171b2ba10" width="180" height="180"></td>
51+
</tr>
52+
<tr align="center">
53+
<td style="text-align:center"><a href="https://github.com/minimo-9"><img src="https://img.shields.io/badge/GitHub-181717?style=for-the-badge&logo=github&logoColor=white"/></a> </td>
54+
<td style="text-align:center"><a href="https://github.com/LeeCh0129"><img src="https://img.shields.io/badge/GitHub-181717?style=for-the-badge&logo=github&logoColor=white"/></a> </td>
55+
<td style="text-align:center"><a href="https://github.com/dltmdals3929"><img src="https://img.shields.io/badge/GitHub-181717?style=for-the-badge&logo=github&logoColor=white"/> </a></td>
56+
<td style="text-align:center"><a href="https://github.com/dkslel1225"><img src="https://img.shields.io/badge/GitHub-181717?style=for-the-badge&logo=github&logoColor=white"/></a> </td>
57+
<td style="text-align:center"><a href="https://github.com/Yun-Jinwoo"><img src="https://img.shields.io/badge/GitHub-181717?style=for-the-badge&logo=github&logoColor=white"/></a></td>
58+
</tr>
59+
</table>
60+
61+
## 🔨 역할 분담
62+
### 박광민
63+
- 공용 컴포넌트(Button, Input) 개발 및 버튼 중복 클릭 방지 처리
64+
- Axios 통신 모듈화 및 API 관리, 에러 발생 시 사용자 알림(Alert) 처리
65+
- 메시지 작성 페이지 반응형 구현 및 메타태그를 활용한 미리보기 설정
66+
- 전체 폰트 스타일 적용 (에디터 및 렌더링 결과물에 클래스 기반 반영)
67+
- 홈 페이지 및 메시지 작성 페이지 애니메이션 효과 구현
68+
- 메시지 작성 페이지(`/post/:id/message`) 구현
69+
- 드롭다운 컴포넌트 개발
70+
- Rich Text Editor(Quill) 커스텀 툴바 구성 (링크, 색상, 리스트 등 포함)
71+
- 프로필 이미지 업로드 기능 및 기본 이미지 클릭 시 초기화 처리
72+
- 폼/에디터 유효성 검사 및 버튼 비활성화 처리
73+
- 프로필 이미지 GET 요청 시 Skeleton UI 적용
74+
- 기존 API 연동(GET) 및 게시글 작성(POST) 기능 구현
75+
- 작성 완료 후 해당 카드 페이지(`/post/:id`)로 이동 처리
76+
- 로컬스토리지 임시 저장 및 관리 기능 개발
77+
- 작성 중 임시 저장
78+
- 제출 시 임시 저장 삭제
79+
- 저장 후 24시간 경과 시 자동 삭제
80+
- 초기 상태 세팅 처리 (기본 이미지, 기본 폰트 등)
81+
82+
### 이찬호
83+
- 프로젝트 초기 세팅
84+
- ESLint, Prettier 등 도구 설정
85+
- 깃허브 프로젝트 관리
86+
- PR 템플릿 설정
87+
- 브랜치 전략 수립 및 관리
88+
- Fork Repository 자동 동기화 워크플로우 구축
89+
- CI/CD 파이프라인 구축
90+
- GitHub Actions 워크플로우 설정
91+
- Vercel 자동배포 환경 구성
92+
- Production/Preview 환경 분리
93+
- 헤더 서비스 구현
94+
- 이모지 리액션 기능 (추가/조회/카운팅)
95+
- Optimstic UI 패턴 적용
96+
- 중복 클릭 방지
97+
- 카카오톡 공유 기능
98+
- URL 클립보드 복사 기능
99+
- 토스트 알림
100+
- 반응형 UI/UX 구현
101+
102+
### 이승민
103+
- 메인 화면 구현
104+
- 반응형 UI/UX 구현
105+
106+
### 전지윤
107+
- 공통 헤더 컴포넌트 구현
108+
- /list 페이지 캐러셀 구현
109+
- 슬라이드 기능: 화면 크기에 따라 버튼, 터치, 드래그 입력 방식으로 동작
110+
- 캐러셀 끝 도달 시 Bounce 애니메이션 적용
111+
- Skeleton UI와 이미지 Preload 처리로 초기 로딩 시 사용자 경험 개선
112+
113+
114+
### 윤진우
115+
- 롤링 페이퍼 생성 페이지 (`/post`) 구현
116+
- 배경 색상 및 이미지 선택 기능 제공
117+
- 배경 이미지 업로드 기능 추가 구현
118+
- 입력된 데이터를 기반으로 API에 POST 요청 후, 생성된 페이지로 이동
119+
- 이미지 로딩 중 스켈레톤 UI를 적용해 사용자 경험 향상
120+
121+
- 롤링 페이퍼 페이지 (`/post/{id}`) 구현
122+
- `IntersectionObserver`를 활용한 무한 스크롤 기능 구현 (메시지를 일정 개수씩 반복 호출)
123+
- 사용자 입력값에 대해 XSS 방지를 위해 `DOMPurify` 적용
124+
- 메시지 클릭 시 상세 내용을 모달 창으로 출력
125+
- 메시지 내용이 길 경우 `...`으로 처리하여 레이아웃 균형 유지
126+
- 뒤로 가기 버튼 추가로 페이지 탐색 편의성 제공
127+
128+
- 롤링 페이퍼 편집 페이지 (`/post/{id}/edit`) 구현
129+
- 메시지 삭제 기능 구현
130+
- 페이지 삭제 기능 구현 (**실수 방지를 위해 2단계 확인창을 적용하여 안전성 강화**)
131+
- 편집 모드에서는 "추가하기" 버튼이 숨겨지므로, 메시지 개수를 조정해 레이아웃의 시각적 균형을 유지
132+
133+
- 사용자 경험(UX) 최적화
134+
- 뒤로 가기 버튼에 지속적인 색상 변화 애니메이션을 적용해, 특정 배경 이미지 위에서도 눈에 띄도록 개선
135+
- 모달 창 등장 및 종료에 부드러운 애니메이션을 적용해 사용자 몰입도 향상
136+
- 메시지 삭제 버튼에 hover 애니메이션 추가로 조작 피드백 제공
137+
138+
## 📂 폴더 구조
139+
``` bash
140+
project-root/
141+
├── src/
142+
│ ├── api/
143+
│ ├── assets/
144+
│ │ ├── images/
145+
│ │ └── styles/
146+
│ ├── components/
147+
│ │ ├── common/
148+
│ │ └── layout/
149+
│ ├── context/
150+
│ ├── hooks/
151+
│ ├── pages/
152+
│ ├── utils/
153+
│ ├── App.jsx
154+
│ └── main.jsx
155+
└── ...
156+
```
157+
158+
## 📝 컨벤션
159+
160+
### 🧐 Commit Type & Emoji Guide
161+
162+
| **commit type** | **description** |
163+
|---------------|----------------|
164+
| feat | ✨ 기능 추가 |
165+
| feat | 🖼️ 아이콘 추가 |
166+
| fix | 🐛 버그 수정 |
167+
| docs | 📝 문서 수정 |
168+
| style | 🎨 UI, 스타일 관련 추가 및 수정 |
169+
| refactor | ♻️ 리팩토링 |
170+
| chore | 🔧 설정, 빌드 변경 |
171+
| chore | 📁 폴더 구조 변경 또는 디렉토리 작업 |
172+
| remove | 🔥 불필요한 코드/파일 제거 |
173+
| deploy | 🚀 프로젝트 배포 |
174+
175+
176+
177+
### 📂 폴더/파일명 네이밍 컨벤션
178+
179+
| **대상** | **규칙** | **예시** |
180+
|------|------|--------|
181+
| 폴더명 | 케밥케이스 (kebab-case) | components, user-profile |
182+
| 컴포넌트 파일명 | 파스칼케이스 (PascalCase) | UserProfile.jsx |
183+
| 스타일 파일명 | 케밥케이스 + .styles.js | user-profile.styles.js |
184+
| 이미지/아이콘 파일명 | 케밥케이스 | logo-icon.png, profile-default.png |
185+
| 함수명/변수명 | 카멜케이스 (camelCase) | fetchUserData, userList |
186+
| 환경변수 | 대문자+스네이크케이스 | VITE_API_URL |
187+
| 클래스명 | BEM 방식 | .block__element--modifier |
188+
189+
### 🖊️ Git Flow
190+
191+
| **브랜치명** | **설명** |
192+
|------------|---------|
193+
| main | 배포 브랜치 |
194+
| develop | 통합 개발 브랜치 |
195+
| feature/* | 기능 개발 브랜치 |
196+
197+
### 🌿 브랜치 네이밍 컨벤션
198+
199+
| **브랜치 종류** | **네이밍 규칙** | **예시** |
200+
|------|------|--------|
201+
| 기능 개발 | feature/{이름} | feature/park |
202+
| 버그 수정 | fix/{버그-설명} | fix/login-button-bug |
203+
| 문서 수정 | docs/{문서-설명} | docs/readme-update |
204+

src/components/Modal/Modal.jsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { useState } from 'react';
1+
import { useRef, useState, useEffect, useCallback } from 'react';
22
import DOMPurify from 'dompurify';
33
import ReactDOM from 'react-dom';
44
import Badge from '../Badge/Badge';
55
import Button from '../common/Button';
6+
import useModalClose from '../../hooks/useModalClose'; // 경로에 맞게 수정
67
import styles from './Modal.module.scss';
78

89
export default function Modal({
@@ -14,19 +15,27 @@ export default function Modal({
1415
createdAt,
1516
onClose,
1617
}) {
18+
const modalRef = useRef(null);
1719
const [isClosing, setIsClosing] = useState(false);
1820
const sanitizedHTML = DOMPurify.sanitize(children);
1921

20-
const handleCloseModal = () => {
22+
// 닫기 로직 (애니메이션 포함)
23+
const handleCloseModal = useCallback(() => {
2124
setIsClosing(true);
2225
setTimeout(() => {
2326
onClose();
2427
}, 300);
25-
};
28+
}, [onClose]);
29+
30+
// 외부 클릭 감지하여 닫기
31+
useModalClose(modalRef, handleCloseModal);
2632

2733
return ReactDOM.createPortal(
2834
<div className={styles.backdrop}>
29-
<article className={`${styles.modal} ${isClosing ? styles.closing : ''}`}>
35+
<article
36+
ref={modalRef}
37+
className={`${styles.modal} ${isClosing ? styles.closing : ''}`}
38+
>
3039
<header className={styles['modal__header']}>
3140
<div className={styles['modal__profile-img']}>
3241
<img src={image} alt="프로필 이미지" />

src/hooks/useModalClose.jsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useEffect } from 'react';
2+
3+
export default function useDetectClose(ref, onClose) {
4+
useEffect(() => {
5+
const handleClick = (e) => {
6+
if (ref.current && !ref.current.contains(e.target)) {
7+
onClose(); // 바로 닫기 말고 외부에서 애니메이션 포함한 onClose 실행
8+
}
9+
};
10+
document.addEventListener('mousedown', handleClick);
11+
return () => {
12+
document.removeEventListener('mousedown', handleClick);
13+
};
14+
}, [ref, onClose]);
15+
}

src/pages/Recipient/Recipient.jsx

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,21 @@ export default function Recipient({ showDelete }) {
5050
new Map(combined.map((message) => [message.id, message])).values(),
5151
);
5252

53-
return uniqueMessages;
54-
});
55-
56-
if (showDelete) {
57-
setMessages(allMessages);
58-
} else {
59-
if (
60-
allMessages.length % 6 === 0 &&
61-
allMessages.length !== newMessages.count
62-
) {
63-
setMessages(allMessages.slice(0, allMessages.length - 1));
53+
if (showDelete) {
54+
setMessages(uniqueMessages);
6455
} else {
65-
setMessages(allMessages);
56+
if (
57+
uniqueMessages.length % 6 === 0 &&
58+
uniqueMessages.length !== newMessages.count
59+
) {
60+
setMessages(uniqueMessages.slice(0, uniqueMessages.length - 1));
61+
} else {
62+
setMessages(uniqueMessages);
63+
}
6664
}
67-
}
65+
66+
return uniqueMessages;
67+
});
6868
if (!postData) return;
6969
setHasNextMessage(offset < postData.messageCount);
7070
setLoading(false);
@@ -82,6 +82,7 @@ export default function Recipient({ showDelete }) {
8282
useEffect(() => {
8383
const observer = new IntersectionObserver((entries) => {
8484
const firstEntry = entries[0];
85+
8586
if (firstEntry.isIntersecting && hasNextMessage && !loading) {
8687
loadMoreMessages();
8788
}
@@ -126,7 +127,7 @@ export default function Recipient({ showDelete }) {
126127
}
127128

128129
function handleGoBack() {
129-
navigate(-1);
130+
showDelete ? navigate(`/post/${id}/`) : navigate('/list');
130131
}
131132

132133
function handleEditClick(id) {

0 commit comments

Comments
 (0)