
﹣ 2023.01.03~2023.02.06 (기획+구현)
[Front-end] - Typescript, React, JavaScript, Router, Sass, Figma(디자인)
[Back-end] - Node.js, Express, Mysql, Typescript
[Communication] - Trello, Discord


swiper를 이용하여 잔디와 날씨기능을 캐러셀로 구현하였고, 로그인 하지 않았을 시에는 날씨부분만 보여지도록 구현하였습니다.

메인페이지의 잔디는 현재 년도의 1월 1일부터 12월 31일까지를 보여주고, 해당 날짜에 블로그 포스팅을 했을 때 잔디에 색이 들어가도록 하였습니다.
const now = new Date(); //현재 날짜
const year = now.getFullYear(); //현재 년도
const start = new Date(year + '-01-01'); //시작일은 현재 년도의 1월 1일
const timeDiff = now.getTime() - start.getTime();
const day = Math.floor(timeDiff / (1000 * 60 * 60 * 24) + 1); ////1월1일부터 오늘날짜까지의 경과일계산
const [postingData, setPostingData] = useState<postDataInterface[]>([]);
////유저의 데이터에 1월1일과 12월 31일 데이터가 있는지 확인
const dayCheck1 = postingData.some(
(data) => data.date === year + '-01-01' && year + '-12-31'
);
const dayCheck2 = postingData.some((data) => data.date === year + '-01-01');
const dayCheck3 = postingData.some((data) => data.date === year + '-12-31');
const firstDate = {
date: '2023-01-01',
count: 0,
level: 0,
};
const lastDate = {
date: '2023-12-31',
count: 0,
level: 0,
};
//1년 범위로 보여주기 위한 데이터 추가
if (dayCheck1 === false) {
postingData.unshift(firstDate);
postingData.push(lastDate);
} else if (dayCheck2 === false) {
postingData.unshift(firstDate);
} else if (dayCheck3 === false) {
postingData.push(lastDate);
}
1년단위의 잔디를 보여주기 위해 1월1일과 12월 31일의 범위를 지정해주었고, 유저가 블로그를 시작한 경과일을 보여주었습니다.
새벽
아침
낮
노을 지는 오후
밤
//현재 시간에 맞추어 (새벽,아침,낮,노을,밤) 으로 저장
useEffect(() => {
const date = new Date();
const hour = date.getHours();
setCurrentHour(hour);
if (hour >= 4 && hour <= 6) {
setTime('dawn');
} else if (hour >= 7 && hour <= 12) {
setTime('morning');
} else if (hour >= 13 && hour <= 15) {
setTime('afternoon');
} else if (hour >= 16 && hour <= 17) {
setTime('dusk');
} else {
setTime('night');
}
getWeather();
}, [currentHour]);
//현재 위치정보 이용 동의 여부와 , 현재 위치의 현재 날씨 정보를 가져옴
const getWeather = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((position) => {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
const url = `https://api.openweathermap.org/data/2.5/weather?&lat=${lat}&lon=${lon}&appid=${process.env.REACT_APP_WEATHER_API_KEY}&units=metric`;
const fetchData = async () => {
try {
const weatherResponse = await fetch(url);
const weather = await weatherResponse.json();
setWeatherData(weather.weather[0].main);
//현재 지역
setLocation(weather.name);
//현재 위치 날씨
setWeatherId(weather.weather[0].id);
//현재 온도
const temp = Math.floor(weather.main.temp) + '°C';
setTemperature(temp);
//위치정보 동의
setAgree(true);
} catch (error) {
console.error('error');
}
};
fetchData();
});
} else {
setAgree(false);
}
};
//현재날씨 한글로 변역
useEffect(() => {
const translateData = async () => {
try {
const weatherTranslateResponse = await fetch('./data/weather.json');
const weatherTranslate = await weatherTranslateResponse.json();
setTranslateData(weatherTranslate.data);
} catch (error) {
console.error('error');
}
};
translateData();
}, []);
{translateData.map((data) => {
const { id, content } = data;
if (id === weatherId) {
return (
<span key={id} className={css.currentWeather}>
{content}
</span>
);
}
})}
☀️ 날씨 api는 openWeatherApi를 사용하였습니다.
날씨기능은 현재 날씨 api와 3시간 간격의 날씨 api 두가지는 가져와 fetch 해주었습니다.
현재 시간에 맞추어 배경색상을 변경해주었고, 아이콘 또한 낮과 밤 2가지 버전으로 넣어주어 확인하기 편하도록 구현하였습니다. 현재 날씨를 한글로 보여주고 싶어 번역파일을 목데이터로 만들어 번역해주었습니다.


눈과 비가 내리는 모습을 위해 조건을 true로 하고 영상을 찍어보았습니다. 이 애니메이션들은 현재 날씨에 맞추어 화면에 나타납니다.
//비내리는 애니메이션 코드
const Rainy = () => {
const RainDrop = ({ style }: any) => {
return (
<p className={css.rainDrop} style={style}>
💧
</p>
);
};
const makeRainDrop = () => {
let animationDelay = '0s';
let fontSize = '5px';
const arr = Array.from({ length: 50 }, (v, i) => i);
return arr.map((el, i) => {
animationDelay = `${-Math.random() * 10}s`; //랜덤 delay
fontSize = `${Math.floor(Math.random() * 20)}px`; //랜덤 물방울 크기
const style = {
animationDelay,
fontSize,
};
return <RainDrop key={i} style={style} />;
});
};
return <div className={css.rainContainer}>{makeRainDrop()}</div>;
};

//타입지정
interface AllPostInterface {
id: string;
title: string;
content: string;
thumbnailImgUrl: string;
createdAt: string;
user: {
id: number;
nickname: string;
profileImgUrl: string;
};
}
//주제 드롭다운 인터페이스
export interface TopicDataInterface {
setTopicIdData: Dispatch<SetStateAction<number>>;
setPagination: Dispatch<SetStateAction<number>>;
}
//전체글 데이터
//주제 아이디
const [topicIdData, setTopicIdData] = useState(0);
//페이지네이션
const [pagination, setPagination] = useState(1);
const [allPostData, setAllPostData] = useState<AllPostInterface[]>([]);
const [maxCountPage, setMaxCountPage] = useState<number>(1);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(
`${process.env.REACT_APP_API_URL}/posts?${
topicIdData !== 0 ? `topicId=${topicIdData}&` : ''
}countPerPage=8&pageNumber=${pagination}`,
{
headers: requestHeaders,
}
);
const json = await response.json();
setMaxCountPage(json.maxPage);
setAllPostData(json.data);
} catch (error) {
console.error('error');
}
};
fetchData();
}, [pagination, topicIdData]);
const prevPage = () => {
setPagination(pagination - 1);
};
const nextPage = () => {
setPagination(pagination + 1);
};
//첫 페이지 일 경우 버튼 안보이도록
{pagination === 1 ? null : (
<FaChevronLeft className={css.chevron} onClick={prevPage} />
)}
//최대 페이지 수 일 경우 버튼 안보이도록
{pagination === maxCountPage ? null : (
<FaChevronRight className={css.chevron} onClick={nextPage} />
)}
주제가 선택되지 않을 시 모든 주제의 전체글을 보여주고, 주제 선택 시 주제에 해당하는 글들만을 보여줍니다. 화살표 버튼 클릭 시 이전과 다음페이지로 이동을 합니다!

이웃이 없을 시

이웃이 있으면 이웃의 글을 보여주고, 주제별로 확인이 가능합니다.
// 팔로잉 여부 확인
const [newPostData, setNewPostData] = useState<NewPostInterface[]>([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(
`${process.env.REACT_APP_API_URL}/following?myFollowing=true`,
{
headers: requestHeaders,
}
);
const json = await response.json();
if (json.data.length === 0) {
setBuddyData(false);
} else {
setBuddyData(true);
}
} catch (error) {
console.error('error');
}
};
fetchData();
}, []);
useEffect(() => {
const fetchData = async () => {
const isSelected = topicIdData !== 0;
try {
const response = await fetch(
`${process.env.REACT_APP_API_URL}/posts?myFollowing=true${
isSelected ? `&topicId=${topicIdData}` : ''
}`,
{
headers: requestHeaders,
}
);
const json = await response.json();
if (json.data.length === 0) {
setBuddyPostData(false);
} else {
setNewPostData(json.data);
setBuddyPostData(true);
}
} catch (error) {
console.error('error');
}
};
fetchData();
}, [topicIdData]);
로그인한 유저가 팔로잉한 유저가 있는지 확인 하였고, 주제를 선택했는지 안했는지에 여부에 따라 fetch를 하였습니다.

const DropDown = ({ setTopicIdData, setPagination }: TopicDataInterface) => {
const token = localStorage.getItem('token');
const requestHeaders: HeadersInit = new Headers();
requestHeaders.set('Content-Type', 'application/json');
if (token) {
requestHeaders.set('Authorization', token);
}
//드롭타운 클릭여부
const [isOpen, setIsOpen] = useState<boolean>(false);
const [topic, setTopic] = useState<string>('주제');
const onToggle = () => setIsOpen(!isOpen);
const onOptionClicked = (value: string, index: number) => () => {
setIsOpen(false);
setTopic(value);
setTopicIdData(index);
setPagination(1);
};
const [topicData, setTopicData] = useState<TopicInterface[]>([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(`${process.env.REACT_APP_API_URL}/topics`);
const json = await response.json();
let topicList = json.data as TopicInterface[];
topicList.push({ content: '전체', id: 0 });
setTopicData(json.data);
} catch (error) {
console.error('error');
}
};
fetchData();
}, []);
{topicData.map((topic) => {
const { id, content } = topic;
return (
<li
key={id}
className={css.topicItem}
onClick={onOptionClicked(content, id)}
>
{content}
</li>
);
})}
주제 데이터를 fetch를 통해 가져오고 해당 주제를 클릭 하였을 때, value값을 저장해주었습니다.


팔로워 클릭 시 팔로워 리스트가 보이고, 팔로잉 클릭 시 팔로잉 리스트가 보입니다.
인원이 많을 시 scroll을 이용하였고, 팔로잉에서 언팔로우 클릭 시 리스트에서 사라지며, 팔로워에서 내가 팔로우 했는지 안했는지 여부를 확인할 수 있고 언팔로우 시 팔로워버튼이 뜨도록 하였습니다.
다른사람의 팔로우에서 내 계정은 버튼이 안보이도록 하였습니다.
유저의 정보를 클릭 시 해당 유저의 블로그로 이동할 수 있습니다.
//팔로워와 팔로잉 페이지 띄우는 조건
const onFollowingBtn = () => {
if (selectFollowingBtn === false) {
setSelectFollowingBtn(true);
setSelectFollowerBtn(false);
} else if (selectFollowingBtn === true) {
setSelectFollowingBtn(true);
}
};
const onFollowerBtn = () => {
if (selectFollowerBtn === false) {
setSelectFollowerBtn(true);
setSelectFollowingBtn(false);
} else if (selectFollowerBtn === true) {
setSelectFollowerBtn(true);
}
};
//팔로우, 언팔로우 버튼
const follow = (value: string, index: number) => () => {
fetch(`${process.env.REACT_APP_API_URL}/follow`, {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify({
targetUsersId: index,
}),
})
.then((res) => res.json())
.then((result) => {
if (result.message === 'FOLLOW_SUCCESSFULLY') {
fetch(`${process.env.REACT_APP_API_URL}/follower/user/${params.id}`, {
headers: requestHeaders,
})
.then((res) => res.json())
.then((res) => setFollowerData(res.data));
}
});
}; // 팔로우 버튼 클릭하고 다시 데이터 fetch하기
const unFollow = (value: string, index: number) => () => {
fetch(`${process.env.REACT_APP_API_URL}/follow/${index}`, {
method: 'DELETE',
headers: requestHeaders,
})
.then((res) => res.json())
.then((result) => {
if (result.message === 'UNFOLLOW_SUCCESSFULLY') {
fetch(`${process.env.REACT_APP_API_URL}/follower/user/${params.id}`, {
headers: requestHeaders,
})
.then((res) => res.json())
.then((res) => setFollowerData(res.data));
}
});
}; //언팔로우 버튼 클릭 후 다시 데이터 fetch!
﹣기획부터 한 프로젝트여서 디자인 부분 또한 많이 신경을 써서 만들었다(날씨 아이콘, 배경, 전체 레이아웃, 팔로우 레이아웃, 로고 등등).그리고 처음으로 타입스크립트를 공부하며 프로젝트를 해보았는데 아직 익숙치 않아 그런지 조금 더 공부를 해야겠다는 생각이 들었다. 부트캠프 수료 후 만든 첫 프로젝트였는데 함께 프로젝트를 해왔던 팀원들과 작업해서 회의뿐만 아닌 소통 모두 수월하고 재미있게 진행했던 것 같다. 중간에 설 연휴로 인해 1주일 조금 안되는 시간 동안은 작업을 할 수 없었어서 기획부터 프로젝트 완성까지 3주정도 소요 된 것 같다. 3주동안 많이 성장하고, 또 많이 즐거웠던 시간들이었다.! 마지막으로 은또 팀원 모두 원하는 곳에 취뽀 성공하길..! 。 ♡ ˙˚ ʚ ᕱ⑅ᕱ ɞ˚˙ ♡ 。