여기를 클릭하시면 Wereads Project가 저장된 원격 저장소로 이동할 수 있습니다.
React
, HTML
, SCSS
, Javascript
, Github
, Git
, Visual Studio Code
1) 로그인
2) 회원가입
3) 메인 쓰레드 목록(List)
4) 쓰레드(포스트) 글 작성하기
5) 쓰레드(포스트) 수정 하기
6) 쓰레드(포스트) 삭제 하기
7) 댓글 기능
(1) Front-End 초기 셋팅
(2) 로그인 페이지
React 코드 - 로그인 페이지)
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import UserInput from '../../Components/UserInput';
import UserButton from '../../Components/UserButton';
import './Login.scss';
const Login = () => {
// 유저 정보(ID, PW)
const [userInfo, setUserInfo] = useState({
email: '',
password: '',
});
// 이메일, 비밀번호
const handleInputChange = event => {
const { name, value } = event.target;
setUserInfo(userInfo => ({
...userInfo,
[name]: value,
}));
};
// 유효성 검사
const isVaild =
userInfo.email.includes('@') &&
userInfo.email.includes('.') &&
userInfo.password.length >= 6;
// 페이지 이동
const navigate = useNavigate();
// 회원가입 페이지 이동
const goSignupPage = () => {
navigate('/signup');
};
// fetch : 로그인 버튼 클릭시 메인 페이지로 이동
const handleLogin = () => {
fetch('/data/Login.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: userInfo.email,
password: userInfo.password,
}),
})
.then(response => response.json())
.then(data => {
if (data.message === 'LOGIN SUCCESS') {
alert('로그인 되었습니다.');
localStorage.setItem('token', data.token);
navigate('/main');
} else {
alert('가입되지 않은 정보입니다.');
}
});
};
return (
<div className="login">
<div className="userFrame" onChange={handleInputChange}>
<div className="imageFrame">
<img className="logo" src="/images/Logo.svg" alt="위코드 로고" />
<img
className="logo"
src="/images/logo_wecode.svg"
alt="위코드 로고"
/>
</div>
<UserInput
type="text"
placeholder="이메일"
value={userInfo.email}
name="email"
onChange={handleInputChange}
/>
<UserInput
type="password"
placeholder="비밀번호"
value={userInfo.password}
name="password"
onChange={handleInputChange}
/>
<UserButton text="로그인" disabled={!isVaild} onClick={handleLogin} />
<div className="buttonWrapper">
<button className="actionButton" onClick={goSignupPage}>
회원 가입
</button>
<div className="wall">|</div>
<button className="actionButton">비밀번호 찾기</button>
</div>
</div>
</div>
);
};
export default Login;
로그인 - Mock Data)
// Login.json
{
"email": "wecode@gmail.com",
"password": "123456"
}
Fetch 메서드 - Mock Data 연결)
fetch('/data/Login.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: userInfo.email,
password: userInfo.password,
}),
})
.then(response => response.json())
.then(data => {
if (data.message === 'LOGIN SUCCESS') {
alert('로그인 되었습니다.');
localStorage.setItem('token', data.token);
navigate('/main');
} else {
alert('가입되지 않은 정보입니다.');
}
});
(3) 회원가입 페이지
React 코드 - 회원가입 페이지)
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import UserInput from '../../Components/UserInput';
import UserButton from '../../Components/UserButton';
import './Signup.scss';
const Signup = () => {
// 이미지 업로드
const [imageName, setImageName] = useState('');
// 유저 정보(필수 사항)
const [userInfo, setUserInfo] = useState({
email: '',
password: '',
passwordConfirm: '',
nickname: '',
});
// 이미지 업로드 input
const handleFileChange = event => {
event.stopPropagation();
const file = event.target.files[0];
if (file) {
setImageName(file.name);
}
};
// 이메일, 비밀번호, 비밀번호 확인
const handleInputChange = event => {
const { name, value } = event.target;
setUserInfo(userInfo => ({
...userInfo,
[name]: value,
}));
};
// isVaild 변수 업데이트
const isVaild =
userInfo.email &&
userInfo.password.length >= 10 &&
userInfo.password === userInfo.passwordConfirm;
// 로그인 페이지 이동
const moveNavigate = useNavigate();
const goToLogin = () => {
moveNavigate('/');
};
// 회원가입 로직
const processSignUp = () => {
fetch('/data/Signup.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: userInfo.email,
password: userInfo.password,
nickname: userInfo.nickname,
}),
})
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error('서버 응답이 올바르지 않습니다.');
})
.then(data => {
if (data.message === 'SIGNUP SUCCESS') {
moveNavigate('/signup-complete');
} else {
alert('회원가입에 실패했습니다. 다시 시도해주세요.');
}
})
.catch(error => {
alert('회원가입 중 오류가 발생했습니다. 다시 시도해주세요.');
});
};
return (
<div className="signup">
<div className="registrationFrame" onChange={handleInputChange}>
<div className="backButtonFrame">
<img
className="backIcon"
src="/images/Back_arrow.svg"
alt="뒤로버튼"
/>
<button className="backButton" onClick={goToLogin}>
뒤로
</button>
</div>
<h1 className="titleText">회원가입</h1>
<div className="infoTextFrame">
<p className="userinfoText">기본 정보</p>
<p className="infoOptionalText">필수 사항</p>
</div>
<div className="userInputFrame">
<UserInput
type="text"
placeholder="이메일"
value={userInfo.email}
name="email"
onChange={handleInputChange}
/>
<UserInput
type="password"
placeholder="비밀번호"
value={userInfo.password}
name="password"
onChange={handleInputChange}
/>
<UserInput
type="password"
placeholder="비밀번호 확인"
value={userInfo.passwordConfirm}
name="passwordConfirm"
onChange={handleInputChange}
/>
</div>
<div className="etcUserFrame">
<div className="infoTextFrame">
<p className="userinfoText">닉네임</p>
<p className="infoOptionalText">선택 사항</p>
</div>
<input
className="nicknameInput"
type="text"
placeholder="닉네임"
value={userInfo.nickname}
name="nickname"
onChange={handleInputChange}
/>
<div className="fileInputFrame">
<label className="fileButton">
<span className="selectFileText">파일 선택</span>
<input
className="fileInput"
type="file"
accept="image/*"
onChange={handleFileChange}
/>
</label>
<input
className="fileDisplay"
placeholder="파일을 선택해 주세요"
value={imageName}
readOnly
/>
</div>
<div className="numberFrame">
<div className="infoTextFrame">
<p className="userinfoText">전화번호</p>
<p className="infoOptionalText">선택 사항</p>
</div>
<div className="numberSelectFrame">
<select className="numberBox">
{PHONENUMBER_LIST.map((number, index) => (
<option key={index}>{number}</option>
))}
</select>
<input
className="numberInput"
type="text"
placeholder="휴대폰 번호를 입력해주세요"
/>
</div>
</div>
<div className="birthdayFrame">
<div className="infoTextFrame">
<p className="userinfoText">생일</p>
<p className="infoOptionalText">선택 사항</p>
</div>
<div className="birthdaySelectFrame">
<select className="birthdayBox yearBox">
{BIRTHDAY_YEAR_LIST.map((year, index) => (
<option key={index}>{year}</option>
))}
</select>
<select className="birthdayBox monthBox">
{BIRTHDAY_MONTH_LIST.map((month, index) => (
<option key={index}>{month}</option>
))}
</select>
<select className="birthdayBox dayBox">
{BIRTHDAY_DAY_LIST.map((day, index) => (
<option key={index}>{day}</option>
))}
</select>
</div>
</div>
<div className="signupButtonFrame">
<UserButton
disabled={!isVaild}
onClick={processSignUp}
text="회원 가입"
/>
</div>
</div>
</div>
</div>
);
};
export default Signup;
// 생일 데이터 : 년,월,일
const BIRTHDAY_YEAR_LIST = Array.from(
{ length: 15 },
(_, i) => `${i + 1990}년`,
);
const BIRTHDAY_MONTH_LIST = Array.from({ length: 12 }, (_, i) => `${i + 1}월`);
const BIRTHDAY_DAY_LIST = Array.from({ length: 31 }, (_, i) => `${i + 1}일`);
// 휴대폰 데이터 : (010)
const PHONENUMBER_LIST = ['010', '011', '016', '018', '019'];
회원가입 - 상수 데이터)
// 생일 데이터 : 년,월,일
const BIRTHDAY_YEAR_LIST = Array.from(
{ length: 15 },
(_, i) => `${i + 1990}년`,
);
const BIRTHDAY_MONTH_LIST = Array.from({ length: 12 }, (_, i) => `${i + 1}월`);
const BIRTHDAY_DAY_LIST = Array.from({ length: 31 }, (_, i) => `${i + 1}일`);
// 휴대폰 데이터 : (010)
const PHONENUMBER_LIST = ['010', '011', '016', '018', '019'];
회원가입 - Mock Data)
{
"email": "wecode@gmail.com",
"password": "123456",
"nickname": "Apple"
}
Fetch 메서드 - Mock Data 연결)
// 회원가입 로직
const processSignUp = () => {
fetch('/data/Signup.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: userInfo.email,
password: userInfo.password,
nickname: userInfo.nickname,
}),
})
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error('서버 응답이 올바르지 않습니다.');
})
.then(data => {
if (data.message === 'SIGNUP SUCCESS') {
moveNavigate('/signup-complete');
} else {
alert('회원가입에 실패했습니다. 다시 시도해주세요.');
}
})
.catch(error => {
alert('회원가입 중 오류가 발생했습니다. 다시 시도해주세요.');
});
};
(4) 회원가입 완료 페이지
React 코드 - 회원가입 완료 페이지)
import React from 'react';
import { useNavigate } from 'react-router-dom';
import UserButton from '../../Components/UserButton';
import './SignupComplete.scss';
const SignupComplete = () => {
const moveNavigate = useNavigate();
// 확인 버튼 (로그인 페이지 이동)
const goToLogin = () => {
moveNavigate('/');
};
// 뒤로 버튼 (회원가입 페이지 이동)
const goToSignup = () => {
moveNavigate('/signup');
};
return (
<div className="signupcomplete">
<div className="backButtonFrame">
<img className="backIcon" src="/images/Back_arrow.svg" alt="뒤로버튼" />
<button className="backButton" onClick={goToSignup}>
뒤로
</button>
</div>
<div className="completeFrame">
<img
className="completelogo"
src="/images/banner_square.svg"
alt="완료체크"
/>
<h1 className="mainText">회원 가입되었습니다!</h1>
<p className="subText">이제 로그인해주세요.</p>
<div className="buttonFrame">
<UserButton text="확인" onClick={goToLogin} />
</div>
</div>
</div>
);
};
export default SignupComplete;
(5) 메인 쓰레드 List 페이지
React 코드 - 메인 쓰레드 List 페이지)
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import UserButton from '../../Components/UserButton';
import './MainThreadList.scss';
const MainThreadList = () => {
// 포스트 리스트 관리
const [postList, setPostList] = useState([]);
// 유저 토큰 가져오기
const userToken = localStorage.getItem('token');
// 페이지 이동
const navigate = useNavigate();
// 포스트 데이터
useEffect(() => {
fetchPostData();
}, [userToken]);
// 포스트 데이터 가져오는 함수
const fetchPostData = () => {
fetch('/data/Postlist.json', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${userToken}`,
},
})
.then(response => {
if (!response.ok) {
throw new Error('네트워크 응답이 올바르지 않습니다');
}
return response.json();
})
.then(data => {
if (Array.isArray(data.data)) {
const sortedPosts = data.data.sort(
(a, b) => new Date(b.createdAt) - new Date(a.createdAt),
);
setPostList(sortedPosts);
} else {
console.error('데이터가 배열이 아닙니다');
}
})
.catch(error => {
console.error(
'데이터를 불러오는 중 오류가 발생했습니다:',
error.message,
);
alert(error.message);
});
};
// 로그인 후 이용가능 함수
const checkAuth = () => {
const userToken = localStorage.getItem('token');
if (!userToken) {
alert('로그인 후 이용할 수 있습니다.');
return false;
}
return true;
};
// 로그인 후 포스트 작성 이용가능 함수
const redirectToLoginPage = () => {
const loginConfirmed = window.confirm(
'로그인이 필요합니다. 로그인 하시겠습니까?',
);
if (loginConfirmed) {
navigate('/');
}
};
// 삭제 권한 로직 (삭제 버튼)
const handleDelete = postId => {
if (!checkAuth()) {
return;
}
const deleteConfirmed = window.confirm('포스트를 삭제하시겠습니까?');
if (deleteConfirmed) {
fetch(`/data/Delete.json`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${userToken}`,
},
body: JSON.stringify({
postId: postId,
}),
})
.then(response => {
if (!response.ok) {
if (response.status === 404) {
throw new Error('포스트 삭제 과정에서 문제가 발생했습니다.');
} else {
throw new Error('포스트 삭제에 실패했습니다.');
}
return;
}
// 포스트가 성공적으로 삭제되면 새로운 리스트 받아오기 함수 실행
fetchPostData();
})
.catch(error => {
console.error('포스트 삭제 오류:', error.message);
alert(error.message);
});
}
};
// 좋아요 로직
const handlelike = postId => {
if (!checkAuth()) {
return;
}
fetch('/data/Postlike.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${userToken}`,
},
body: JSON.stringify({
postId: postId,
}),
})
.then(response => {
if (!response.ok) {
if (response.status === 404) {
throw new Error('포스트 좋아요 처리 중 문제가 발생했습니다.');
} else {
throw new Error('서버 요청 오류');
}
}
return response.json();
})
.then(data => {
// 서버에서 받은 정보로 상태 업데이트
fetchPostData();
updateLike();
})
.catch(error => {
console.error('Error:', error);
});
};
// 서버 응답 이후에 상태를 업데이트하는 부분 수정
const updateLike = (postId, isLiked) => {
const updatedPostList = postList.map(post => {
if (post.postId === postId) {
return {
...post,
isLiked: isLiked,
};
}
return post;
});
setPostList(updatedPostList);
};
// 수정 권한 로직 (수정 버튼)
const handleEdit = postId => {
if (!checkAuth()) {
} else {
navigate(`/post-edit/${postId}`);
}
};
// 포스트 작성 페이지 이동
const handlePostAdd = () => {
if (!checkAuth()) {
redirectToLoginPage();
} else {
navigate('/post-add');
}
};
// 댓글 페이지 이동
const handleComment = postId => {
if (!checkAuth()) {
} else {
navigate(`/comment/${postId}`);
}
};
return (
<div className="mainthread">
<div className="scrollWrapper">
{postList.map((post, index) => (
<div className="postListFrame" key={index}>
<div className="postList">
<img
className="profileImages"
src={post.profileImage}
alt="프로필 사진"
/>
<span className="profileNameTexts">{post.nickname}</span>
<span className="dateTexts">{post.createdAt}</span>
{post.isUser && (
<div>
<button
className="actionButtons deleteButton"
onClick={() => handleDelete(post.postId)}
>
삭제
</button>
<button
className="actionButtons editButton"
onClick={() => handleEdit(post.postId)}
>
수정
</button>
</div>
)}
</div>
<div className="postContentFrame">
<p className="contentTexts">{post.content}</p>
<div className="likeCommentFrame">
<p className="likeTexts">좋아요 {post.likeCount}</p>
<p
className="commentTexts"
onClick={() => handleComment(post.postId)}
>
{post.comments}
</p>
</div>
<img
className="likeHearts"
onClick={() => handlelike(post.postId, post.isLiked)}
src={
post.isLiked ? '/images/likeHeart.svg' : '/images/heart.svg'
}
alt="좋아요"
/>
</div>
</div>
))}
</div>
<div className="footer">
<div className="actionButtonFrame">
<UserButton text="글 쓰기" onClick={handlePostAdd} />
</div>
</div>
</div>
);
};
export default MainThreadList;
React 코드 - 로그인 후 이용가능 로직)
// 로그인 후 이용가능 함수
const checkAuth = () => {
const userToken = localStorage.getItem('token');
if (!userToken) {
alert('로그인 후 이용할 수 있습니다.');
return false;
}
return true;
};
// 로그인 후 포스트 작성 이용가능 함수
const redirectToLoginPage = () => {
const loginConfirmed = window.confirm(
'로그인이 필요합니다. 로그인 하시겠습니까?',
);
if (loginConfirmed) {
navigate('/');
}
};
React 코드 - 권한 에러 로직)
// 삭제 권한 로직
... 생략 ...
.then(response => {
if (!response.ok) {
if (response.status === 404) {
throw new Error('포스트 삭제 과정에서 문제가 발생했습니다.');
} else {
throw new Error('포스트 삭제에 실패했습니다.');
}
return;
}
// 포스트가 성공적으로 삭제되면 새로운 리스트 받아오기 함수 실행
fetchPostData();
})
.catch(error => {
console.error('포스트 삭제 오류:', error.message);
alert(error.message);
});
...
// 좋아요 권한 로직
... 생략 ...
.then(response => {
if (!response.ok) {
if (response.status === 404) {
throw new Error('포스트 좋아요 처리 중 문제가 발생했습니다.');
} else {
throw new Error('서버 요청 오류');
}
}
return response.json();
})
...
메인 쓰레드 List - Mock Data)
{
"data": [
{
"postId": 1,
"nickname": "drawing_kim95",
"profileImage": "https://img.jakpost.net/c/2020/08/26/2020_08_26_103148_1598423727._large.jpg",
"content": "심심한데 그림 그리고 싶어...오늘은 뭐그릴까?",
"createdAt": "2023-08-21",
"updatedAt": "2023-08-22",
"comments": "댓글 6",
"isLiked": false,
"likeCount": 0,
"isUser": true
},
{
"postId": 2,
"nickname": "seongho",
"profileImage": "https://hamtopia.com/web/product/tiny/202306/4990d9e0c64dcf0342ee120905977429.jpg",
"content": "배고파 오늘은 뭐먹지?",
"createdAt": "2023-08-20",
"updatedAt": "2023-08-21",
"comments": "댓글 5",
"isLiked": false,
"likeCount": 2,
"isUser": false
},
{
"postId": 3,
"nickname": "wecode",
"profileImage": "https://www.chemicalnews.co.kr/news/photo/202107/4159_10727_3352.png",
"content": "일라이자 효과는 인간의 사고 과정과 감정을 AI 시스템에 잘못 돌리는 사람들의 경향을 말하며, 따라서 시스템이 실제보다 더 지능적이라고 믿는다. 이 현상은 1966년 MIT 교수 조셉 웨이젠바움이 만든 챗봇인 ELIZA의 이름을 따서 명명되었다.",
"createdAt": "2023-08-19",
"updatedAt": "2023-08-20",
"comments": "댓글 3",
"isLiked": false,
"likeCount": 5,
"isUser": false
},
{
"postId": 4,
"nickname": "guraeng",
"profileImage": "https://img.freepik.com/premium-photo/fluffy-dwarf-hamster-lies-front-view_109543-618.jpg",
"content": "일라이자 효과는 인간의 사고 과정과 감정을....",
"createdAt": "2023-08-18",
"updatedAt": "2023-08-19",
"comments": "댓글 4",
"isLiked": false,
"likeCount": 3,
"isUser": false
},
{
"postId": 5,
"nickname": "wecode_hong",
"profileImage": "https://img.famtimes.co.kr/resources/2018/05/22/7J6ugOP2q6T7Z9Xy.jpg",
"content": "성별과 성격과 같은 인간의 특성을 AI 음성 비서에게 돌리기",
"createdAt": "2023-08-17",
"updatedAt": "2023-08-18",
"comments": "댓글 10",
"isLiked": false,
"likeCount": 7,
"isUser": false
},
{
"postId": 6,
"nickname": "weread",
"profileImage": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS7Xyj1ftWWW5ZjpyALJbMJ-mZwZLGAj_JLsTVAL2MKF7ULNCj7KGR5V6f4uJP8Msr7H3s&usqp=CAU",
"content": "에러 체크 잘하기",
"createdAt": "2023-08-16",
"updatedAt": "2023-08-17",
"comments": "댓글 20",
"isLiked": false,
"likeCount": 9,
"isUser": false
}
]
}
Fetch 메서드 - Mock Data 연결)
// 포스트 데이터
useEffect(() => {
fetchPostData();
}, [userToken]);
// 포스트 데이터 함수
const fetchPostData = () => {
fetch('/data/Postlist.json', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${userToken}`,
},
})
.then(response => {
if (!response.ok) {
throw new Error('네트워크 응답이 올바르지 않습니다');
}
return response.json();
})
.then(data => {
if (Array.isArray(data.data)) {
const sortedPosts = data.data.sort(
(a, b) => new Date(b.createdAt) - new Date(a.createdAt),
);
setPostList(sortedPosts);
} else {
console.error('데이터가 배열이 아닙니다');
}
})
.catch(error => {
console.error(
'데이터를 불러오는 중 오류가 발생했습니다:',
error.message,
);
alert(error.message);
});
};
(6) 포스트 작성 페이지
React 코드 - 포스트 작성 페이지)
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import './PostAdd.scss';
const PostAdd = () => {
// 포스트 작성(저장)
const [postContent, setPostContent] = useState('');
// 유저 토큰, 닉네임, 프로필 이미지
const userToken = localStorage.getItem('token');
const nickName = localStorage.getItem('nickname');
const profileImage = localStorage.getItem('profileImage');
// 페이지 이동 하기
const navigate = useNavigate();
// 포스트 취소
const handleCancel = () => {
if (window.confirm('포스트 작성을 취소하시겠습니까?')) {
window.alert('작성이 취소되었습니다.');
navigate('/main-thread-list');
}
};
// 작성한 포스트 등록하기
const handlePost = () => {
if (!postContent.trim()) {
alert('포스트를 작성해주세요.');
return;
}
const postConfirmed = window.confirm('포스트를 등록하시겠습니까?');
if (postConfirmed) {
fetch('/data/Create.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
Authorization: `Bearer ${userToken}`,
},
body: JSON.stringify({
content: postContent,
}),
})
.then(response => {
if (!response.ok) {
throw new Error('네트워크 응답이 올바르지 않습니다');
}
return response.json();
})
.then(data => {
if (data.message === 'UPLOAD SUCCESS') {
alert('등록이 완료되었습니다.');
navigate('/main-thread-list');
} else {
alert('등록에 실패했습니다. 다시 시도해주세요.');
}
});
}
};
return (
<div className="postAdd">
<div className="profileWrapper">
<div className="profileContainer">
<img className="profileImages" src={profileImage} alt="프로필 사진" />
<span className="profileText">{nickName}</span>
</div>
<div className="postInputContainer">
<textarea
className="postInput"
placeholder="스레드를 시작하세요."
value={postContent}
onChange={event => setPostContent(event.target.value)}
/>
</div>
<div className="postButtonContainer">
<button className="postButtons cancelButton" onClick={handleCancel}>
취소
</button>
<button className="postButtons actionButton" onClick={handlePost}>
게시
</button>
</div>
</div>
</div>
);
};
export default PostAdd;
포스트 작성 Mock Data)
{
"content": "유저가 작성 한 글"
}
Fetch 메서드 - Mock Data 연결)
const postConfirmed = window.confirm('포스트를 등록하시겠습니까?');
if (postConfirmed) {
fetch('/data/Create.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
Authorization: `Bearer ${userToken}`,
},
body: JSON.stringify({
content: postContent,
}),
})
.then(response => {
if (!response.ok) {
throw new Error('네트워크 응답이 올바르지 않습니다');
}
return response.json();
})
.then(data => {
if (data.message === 'UPLOAD SUCCESS') {
alert('등록이 완료되었습니다.');
navigate('/main-thread-list');
} else {
alert('등록에 실패했습니다. 다시 시도해주세요.');
}
});
}
(7) 포스트 수정 페이지
React 코드 - 포스트 수정 페이지)
import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import './PostEdit.scss';
const PostEdit = () => {
// 수정할 포스트 저장
const [editContent, setEditContent] = useState('');
// 유저 정보
const userToken = localStorage.getItem('token');
const nickName = localStorage.getItem('nickname');
const profileImage = localStorage.getItem('profileImage');
// postId 가져오기
const { id: postId } = useParams();
// 해당 postId의 포스트 데이터 가져오기
useEffect(() => {
fetch(`/data/Postlist.json?id=${postId}`, {
method: 'GET',
Authorization: `Bearer ${userToken}`,
})
.then(response => {
if (!response.ok) {
throw new Error('네트워크 응답이 올바르지 않습니다');
}
return response.json();
})
.then(data => {
// 가져온 데이터에서 해당 postId의 내용을 가져와서 수정창에 표시
if (data) {
setEditContent(data.content);
} else {
throw new Error('포스트를 찾을 수 없습니다');
}
})
.catch(error => {
alert('포스트 데이터를 불러오던 중 오류가 발생했습니다.');
});
}, [postId, userToken]);
// 페이지 이동 하기 : 취소
const navigate = useNavigate();
const handleCancel = () => {
if (window.confirm('수정을 취소하시겠습니까?')) {
window.alert('수정이 취소되었습니다.');
navigate('/main-thread-list');
}
};
// 포스트 수정 로직
const handleEdit = postId => {
if (!editContent.trim()) {
alert('수정할 내용을 입력해주세요.');
return;
}
const editConfirmed = window.confirm('게시글을 수정하시겠습니까?');
if (editConfirmed) {
fetch('/data/Update.json', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${userToken}`,
},
body: JSON.stringify({
content: editContent,
postId: postId,
}),
})
.then(response => {
if (!response.ok) {
if (response.status === 403) {
throw new Error(
'권한이 없습니다. 본인이 작성한 포스트만 수정할 수 있습니다.',
);
} else if (response.status === 404) {
throw new Error(
'CONTENT_NOT_FOUND: 해당 스레드를 찾을 수 없습니다.',
);
} else {
throw new Error('네트워크 응답이 올바르지 않습니다');
}
}
return response.json();
})
.then(data => {
if (data.message === 'EDIT SUCCESS') {
alert('수정이 완료되었습니다.');
navigate('/main-thread-list');
} else {
alert('수정에 실패했습니다. 다시 시도해주세요.');
}
})
.catch(error => {
if (error.message.includes('권한이 없습니다')) {
alert(error.message);
} else if (error.message.includes('CONTENT_NOT_FOUND')) {
alert(error.message);
} else {
alert(`오류가 발생했습니다: ${error.message}`);
}
});
}
};
return (
<div className="postEdit">
<div className="profileWrapper">
<div className="profileContainer">
<img className="image" src={profileImage} alt="프로필 사진" />
<span className="nickName">{nickName}</span>
</div>
<div className="editInputContainer">
<textarea
className="editInput"
placeholder="내용 수정하기"
value={editContent}
onChange={e => setEditContent(e.target.value)}
/>
</div>
<div className="editButtonContainer">
<button className="editButtons cancelButton" onClick={handleCancel}>
취소
</button>
<button className="editButtons actionButton" onClick={handleEdit}>
게시
</button>
</div>
</div>
</div>
);
};
export default PostEdit;
포스트 수정 Mock Data)
{
"postId": 1,
"content": "수정한 게시글의 내용"
}
Fetch 메서드 - Mock Data 연결)
const editConfirmed = window.confirm('게시글을 수정하시겠습니까?');
if (editConfirmed) {
fetch('/data/Update.json', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${userToken}`,
},
body: JSON.stringify({
content: editContent,
postId: postId,
}),
})
.then(response => {
if (!response.ok) {
if (response.status === 403) {
throw new Error(
'권한이 없습니다. 본인이 작성한 포스트만 수정할 수 있습니다.',
);
} else if (response.status === 404) {
throw new Error(
'CONTENT_NOT_FOUND: 해당 스레드를 찾을 수 없습니다.',
);
} else {
throw new Error('네트워크 응답이 올바르지 않습니다');
}
}
return response.json();
})
.then(data => {
if (data.message === 'EDIT SUCCESS') {
alert('수정이 완료되었습니다.');
navigate('/main-thread-list');
} else {
alert('수정에 실패했습니다. 다시 시도해주세요.');
}
})
.catch(error => {
if (error.message.includes('권한이 없습니다')) {
alert(error.message);
} else if (error.message.includes('CONTENT_NOT_FOUND')) {
alert(error.message);
} else {
alert(`오류가 발생했습니다: ${error.message}`);
}
});
}
};
(8) 댓글 작성 페이지
React 코드 - 댓글 작성 페이지)
import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import './Comment.scss';
import UserInput from '../../Components/UserInput';
const Comment = () => {
// 댓글 리스트 관리
const [commentList, setCommentList] = useState([]);
// 유저 토큰
const userToken = localStorage.getItem('token');
// 유저 프로필, 닉네임
const nickName = localStorage.getItem('nickname');
const profileImage = localStorage.getItem('profileImage');
// postId 가져오기
const { id: postId } = useParams();
// 페이지 이동
const navigate = useNavigate();
// 댓글 리스트 데이터
useEffect(() => {
fetch(`/data/Commentlist.json?id=${postId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${userToken}`,
},
})
.then(response => {
if (!response.ok) {
throw new Error('네트워크 응답이 올바르지 않습니다');
}
return response.json();
})
.then(data => {
if (Array.isArray(data.data)) {
const sortedPosts = data.data.sort(
(a, b) => new Date(b.createdAt) - new Date(a.createdAt),
);
setCommentList(sortedPosts);
} else {
console.error('데이터가 배열이 아닙니다');
}
})
.catch(error => {
console.error(
'데이터를 불러오는 중 오류가 발생했습니다:',
error.message,
);
alert(error.message);
});
}, [userToken]);
// 메인 스레드 페이지 이동
const handleBack = () => {
if (window.confirm('댓글 작성을 취소하시겠습니까?')) {
window.alert('댓글 작성이 취소되었습니다.');
navigate('/main-thread-list');
}
};
return (
<div className="comment">
<div className="commentWrapper">
<div className="backButtonContainer">
<img
className="backButtonIcon"
src="/images/Back_arrow.svg"
alt="뒤로 아이콘"
/>
<button className="backButton" onClick={handleBack}>
뒤로
</button>
</div>
<div className="userPostContainer">
<img className="profileImages" src={profileImage} />
<span className="profileNames">{nickName}</span>
<span className="dayTexts">2023-08-21</span>
</div>
<div className="mainPostFrame">
<span className="postText">
심심한데 그림 그리고 싶어...오늘은 뭐그릴까?
</span>
<p className="postCommentText">댓글 6</p>
</div>
<div className="commentActionsFrame">
<UserInput type="text" placeholder="댓글을 작성해주세요." />
<button className="actionButton">댓글 개시</button>
</div>
<div className="listContainer">
{commentList.map((comment, index) => (
<div className="commentListFrame" key={index}>
<img
className="profileImages"
src={comment.profileImage}
alt="프로필 사진"
/>
<div className="userInfo">
<span className="profileNames">{comment.nickname}</span>
<p className="commentText">{comment.comment}</p>
</div>
<div className="dayUpdates">
<span className="updateText">{comment.updatedAt}</span>
{comment.isUser && (
<div>
<button className="actionButtons deleteButton">삭제</button>
<button className="actionButtons editButton">수정</button>
</div>
)}
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default Comment;
댓글 List Mock Data)
{
"data": [
{
"postId": 1,
"nickname": "drawing_kim95",
"profileImage": "/images/profileImage_03.jpg",
"comment": "오늘은 너가 그리고 싶은거 위주로 그려봐!",
"updatedAt": "2023.08.21",
"isUser": true
},
{
"postId": 2,
"nickname": "seongho",
"profileImage": "/images/profileImage_01.jpg",
"comment": "작가님! 오늘은 저 그려주세요!",
"updatedAt": "2023.08.22",
"isUser": false
},
{
"postId": 3,
"nickname": "wecode",
"profileImage": "/images/profileImage_02.jpg",
"comment": "이번엔 음식을 한번 그려보는건 어때?",
"updatedAt": "2023.08.23",
"isUser": false
},
{
"postId": 4,
"nickname": "guraeng",
"profileImage": "/images/profileImage_04.jpg",
"comment": "내 마스코트 그려주세요! 비용 드릴께요!",
"updatedAt": "2023.08.24",
"isUser": false
},
{
"postId": 5,
"nickname": "wecode_hong",
"profileImage": "/images/profileImage_05.jpg",
"comment": "저번에 전시회 준비하는거 같던데 전시회 한번 준비해보셔야죠!",
"updatedAt": "2023.08.25",
"isUser": false
},
{
"postId": 6,
"nickname": "wecode_lee",
"profileImage": "/images/profileImage_06.jpg",
"comment": "그림 작가시니 그림과 관련된 프로젝트 하나 만들어 보시죠!",
"updatedAt": "2023.08.26",
"isUser": false
},
{
"postId": 7,
"nickname": "shk_9505",
"profileImage": "/images/profileImage_03.jpg",
"comment": "같이 여행가면서 그림이나 그리자 ㅋㅋ 그림 챙기고 얼렁 나오셈!",
"updatedAt": "2023.08.27",
"isUser": false
},
{
"postId": 8,
"nickname": "juyoung_0211",
"profileImage": "/images/profileImage_05.jpg",
"comment": "우왕..대단해!",
"updatedAt": "2023.08.28",
"isUser": false
}
]
}
Fetch 메서드 - Mock Data 연결)
// 댓글 리스트 데이터
useEffect(() => {
fetch(`/data/Commentlist.json?id=${postId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${userToken}`,
},
})
.then(response => {
if (!response.ok) {
throw new Error('네트워크 응답이 올바르지 않습니다');
}
return response.json();
})
.then(data => {
if (Array.isArray(data.data)) {
const sortedPosts = data.data.sort(
(a, b) => new Date(b.createdAt) - new Date(a.createdAt),
);
setCommentList(sortedPosts);
} else {
console.error('데이터가 배열이 아닙니다');
}
})
.catch(error => {
console.error(
'데이터를 불러오는 중 오류가 발생했습니다:',
error.message,
);
alert(error.message);
});
}, [userToken]);
[기술 문제]
- 불 특정 사용자가 로그인을 하지 않은 상태에서 포스트 작성 및 수정 하려고 시도할 때, 유저 권한 에러가 발생하자 않아, 사용자의 정보를 마음대로 수정하거나 삭제할 수 있는 윤리적 문제가 발생할 기술 문제의 위험성을 감지하였습니다.
[고민]
- 이러한 기술적인 문제에 대응하기 위해서 로그인하지 않은 사용자는 포스트 이용 권한을 제한하는 방법을 선택해서 윤리적 문제를 해결하고, 사용자들에게 안전한 커뮤니티 경험을 제공해야 할 필요성을 느끼게 되었습니다.
[시도 방법]
- 로그인하지 않은 불 특정 사용자가 다른 유저 포스트에 좋아요를 누르거나 수정 또는 삭제하려고 시도할 때, fetch 메서드를 활용해 localstorge에 유저 토큰의 유무를 확인 및 404 권한 에러를 발생시키도록 기능을 수정하여, 로그인이 안된 상태에서도 포스트를 작성하거나 수정 또는 삭제 할 경우에는, 로그인 후 이용해달라는 알림 문구를 띄워서 문제를 보완했습니다.
[개선 성과]
- 유저 권한 에러를 통해 로그인하지 않은 사용자들이 다른 사용자의 정보를 수정하거나 삭제하는 시도를 차단하여, 사용자들이 안전하고 신뢰할 수 있는 커뮤니티를 경험할 수 있도록 보완하였습니다. 이를 통해 사용자들이 안심하고 커뮤니티를 이용할 수 있도록 개선했습니다.
1) 가장 뿌듯했다고 느꼈던 코드
// 포스트 데이터
useEffect(() => {
fetchPostData();
}, [userToken]);
// 포스트 데이터 가져오는 함수
const fetchPostData = () => {
fetch('/data/Postlist.json', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${userToken}`,
},
})
.then(response => {
if (!response.ok) {
throw new Error('네트워크 응답이 올바르지 않습니다');
}
return response.json();
})
.then(data => {
if (Array.isArray(data.data)) {
const sortedPosts = data.data.sort(
(a, b) => new Date(b.createdAt) - new Date(a.createdAt),
);
setPostList(sortedPosts);
} else {
console.error('데이터가 배열이 아닙니다');
}
})
.catch(error => {
console.error(
'데이터를 불러오는 중 오류가 발생했습니다:',
error.message,
);
alert(error.message);
});
};
2) 아쉬웠던 코드
... 생략 ...
// 좋아요 로직
const handlelike = postId => {
if (!checkAuth()) {
return;
}
fetch('/data/Postlike.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${userToken}`,
},
body: JSON.stringify({
postId: postId,
}),
})
.then(response => {
if (!response.ok) {
if (response.status === 404) {
throw new Error('포스트 좋아요 처리 중 문제가 발생했습니다.');
} else {
throw new Error('서버 요청 오류');
}
}
return response.json();
})
.then(data => {
// 서버에서 받은 정보로 상태 업데이트
fetchPostData();
updateLike();
})
.catch(error => {
console.error('Error:', error);
});
};
// 서버 응답 이후에 상태를 업데이트하는 부분 수정
const updateLike = (postId, isLiked) => {
const updatedPostList = postList.map(post => {
if (post.postId === postId) {
return {
...post,
isLiked: isLiked,
};
}
return post;
});
setPostList(updatedPostList);
};
... 생략 ...
<div className="likeCommentFrame">
<p className="likeTexts">좋아요 {post.likeCount}</p>
<p
className="commentTexts"
onClick={() => handleComment(post.postId)}
>
{post.comments}
</p>
</div>
<img
className="likeHearts"
onClick={() => handlelike(post.postId, post.isLiked)}
src={post.isLiked ? '/images/likeHeart.svg' : '/images/heart.svg'}
alt="좋아요"
/>
... 생략 ...
wereads 프로젝트를 끝내고 9월부터 지금까지 순간들을 돌아봤을때, 지금 이 회고록을 작성하는 순간에도 그리고 개발자로써 필요한 마인드는 무엇일까?에 대해서 스스로 생각할 수 있는 시간을 가지면서 프로젝트에 임했던거 같아요. 특히 멘토님의 빡센 코드리뷰를 어떻게 빠르게 적용해야 하는지 고민이 많았는데 코드리뷰 덕분에 코드를 어떻게 적용해야 좋은 코드이고 지저분한 스파게티 코드 한줄을 어떻게 줄여볼 수 있을까를 생각하는 습관을 가지게 되면서 개발자는 끊임없는 도전과 성장으로 업그레이드 된다는걸 체감했습니다. 먼저, 9월부터 지금 현재 순간을 돌아보며 성장했다고 느꼈던 점을 회고하면 다음과 같습니다.
뿌듯했던 점)
아쉬웠던 점)
이렇게 프로젝트를 통해 뿌듯했던 점과, 아쉬웠던 점을 회고할 수 있음에 감사합니다😄 추후에 아쉽게 느꼈던 점들이 하나의 장점으로 현업에서도 적용하기 위해 노력한다면, 이 아쉬움도 장점으로 만들 수 있다고 생각했어요:) 제 자신 스스로에게 적용하는 방법을 알아가는 과정이라서 그런거지, 제 자신이 애초에 못한다고 생각하지 않거든요:)
이제 시작입니다. 위코드를 졸업하고 나서, 본격적인 개발자의 꿈을 향해 나아갈 준비를 하고 있지만, 생각해보면 저는 과연 어떤 개발자인지 생각을 하지 못했는데, 결론적으론 아직까지 부족한 실력이더라도 개발에 열정이 있고 진심으로 행동하기 위해 노력하는 사람이라는걸 스스로 돌아보게 되었습니다. 그렇다고 완벽하게 잘하는 개발자는 아니예요! ㅋㅋㅋㅋ 제 목표는 저보다 어려운 삶을 개선시키는 개발자가 되는게 사명이자 목표니까요 그 꿈을 포기 하지않고 나아가는 저와 응원해주시는 모든 분들께 다시한번 감사하다는 말씀 드리고 싶습니다:)
그동안 수고 많았어요:)
다음도 차근차근 준비해봅시다!!
Fighting!