Skip to content

Commit 3e76d76

Browse files
cho-sohyunxnelb013khakisage
authored
[Feat] 메인 기능 추가 (#14)
* [Feat] : 게시글작성 초안 * [Feat] : 메인페이지 * [Feat] : 회원가입 기능 완성(blob버전) * [Feat] : ui 수정 및 날씨 키워드 추가 , 키워드별 분리 * [Feat] : useCallback 사용 * [Fix] : 엔터 시 키워드 두번 입력되는 문제 수정 * ✨ [Feat] : msw handler 추가 - 회원가입 이메일 인증 로직 추가 - 소셜 로그인 추가 - 회원탈퇴 추가 - 로그아웃 추가 - 날씨 정보 조회 추가 * [✨Feat] : 캐러셀 추가 * 🚑️[Fix]: 게시글 저장 handlers - 기존 FileList 방식에서 Blob 처리 방식으로 변경 - 이와 관련된 postEditor 및 atom 변경 * 🔧[Chore] : 주석 제거 * 🔧 [Chore] : 변수 변경 * 🔧 [Chore] : 변수 변경 * ✨[Feat] : 날씨 기능 추가 * ✨ [Feat] : msw handler 추가 - 게시글 불러오기 api (한명의 유저가 작성한 게시글) - 날씨 추천 게시글 api (현재 날씨 계절 정보에 따른 게시글) * ✨[Feat] : 날씨 추천 기능 * ✨[Feat] : 무한스크롤 기능 추가 --------- Co-authored-by: Eunseok Choi <[email protected]> Co-authored-by: khakisage <[email protected]>
1 parent 00f71a3 commit 3e76d76

File tree

16 files changed

+1154
-100
lines changed

16 files changed

+1154
-100
lines changed

mocks/handlers.ts

Lines changed: 260 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,62 @@
11
import { auth } from "@/firebase/firebaseAuth";
22
import { db } from "@/firebase/firebaseDB";
3-
import { createUserWithEmailAndPassword, signInWithEmailAndPassword } from "firebase/auth";
4-
import { collection, doc, getDoc, getDocs, query, setDoc, where } from "firebase/firestore";
3+
import { storage } from "@/firebase/firebaseStorage";
4+
import { v4 as uuidv4 } from "uuid";
5+
import {
6+
GoogleAuthProvider,
7+
createUserWithEmailAndPassword,
8+
deleteUser,
9+
sendEmailVerification,
10+
signInWithEmailAndPassword,
11+
signInWithPopup,
12+
} from "firebase/auth";
13+
import {
14+
addDoc,
15+
collection,
16+
deleteDoc,
17+
doc,
18+
getDoc,
19+
getDocs,
20+
query,
21+
serverTimestamp,
22+
setDoc,
23+
where,
24+
} from "firebase/firestore";
25+
import { getDownloadURL, ref, uploadBytes } from "firebase/storage";
526
import { rest } from "msw";
27+
import axios from "axios";
28+
29+
// type postObj = {
30+
// email: string;
31+
// content: string;
32+
// dynamicKeywords: string[];
33+
// staticKeywords: string[];
34+
// seasonKeywords: string;
35+
// weatherKeywords: string;
36+
// images: object[];
37+
// createdAt: object;
38+
// };
639

740
export const handlers = [
841
// 회원가입 mocking API
942
rest.post("http://localhost:3000/signup", async (req, res, ctx) => {
1043
const { email, password, keywords, gender, height, weight, nickname } = await req.json();
1144
try {
12-
await createUserWithEmailAndPassword(auth, email, password);
13-
await setDoc(doc(db, "users", email), {
14-
email,
15-
gender,
16-
height,
17-
weight,
18-
keywords,
19-
nickname,
20-
});
45+
await createUserWithEmailAndPassword(auth, email, password)
46+
.then((userCredential) => {
47+
// 이메일 인증 메일 보내기.
48+
sendEmailVerification(userCredential.user);
49+
})
50+
.then(() => {
51+
setDoc(doc(db, "users", email), {
52+
email,
53+
gender,
54+
height,
55+
weight,
56+
keywords,
57+
nickname,
58+
});
59+
});
2160
return res(ctx.status(201), ctx.json({ success: true }), ctx.json({ message: "회원 가입에 성공하였습니다." }));
2261
} catch (error) {
2362
return res(ctx.status(400), ctx.json({ success: false }), ctx.json({ message: "회원 가입에 실패하였습니다." }));
@@ -89,15 +128,15 @@ export const handlers = [
89128
);
90129
}
91130
}),
92-
//닉네임 중복검사
131+
// 닉네임 중복검사
93132
rest.post("http://localhost:3000/nicknamecheck", async (req, res, ctx) => {
94133
const { nickname } = await req.json();
95134
const userRef = collection(db, "users");
96135
const q = query(userRef, where("nickname", "==", nickname));
97136
const querySnapshot = await getDocs(q);
98-
const emailExists = !querySnapshot.empty;
137+
const nicknameExists = !querySnapshot.empty;
99138
try {
100-
if (emailExists === true) {
139+
if (nicknameExists === true) {
101140
// alert("이미 존재하는 닉네임입니다.");
102141
return res(ctx.status(200), ctx.json({ success: true, message: "이미 존재하는 닉네임입니다." }));
103142
} else {
@@ -112,13 +151,216 @@ export const handlers = [
112151
);
113152
}
114153
}),
115-
// 로그아웃 mocking API
116-
rest.get("http://localhost:3000/signout", async (req, res, ctx) => {
154+
// google 로그인 mocking API
155+
rest.get("http://localhost:3000/googlelogin", async (req, res, ctx) => {
156+
const provider = new GoogleAuthProvider();
117157
try {
158+
const userCredential = await signInWithPopup(auth, provider);
159+
const { email } = userCredential.user;
160+
await setDoc(doc(db, "users", email as string), {
161+
email,
162+
});
163+
localStorage.setItem("user", JSON.stringify(userCredential.user));
164+
return res(ctx.status(200), ctx.json({ success: true }), ctx.json({ message: "로그인에 성공하였습니다." }));
165+
} catch (error) {
166+
return res(ctx.status(400), ctx.json({ success: false }), ctx.json({ message: "로그인에 실패하였습니다." }));
167+
}
168+
}),
169+
// 회원탈퇴 mocking API
170+
rest.delete("http://localhost:3000/deleteuser", async (req, res, ctx) => {
171+
const user = localStorage.getItem("user");
172+
const { email } = JSON.parse(user as string);
173+
const curUser = auth.currentUser;
174+
try {
175+
await deleteUser(curUser!);
176+
await deleteDoc(doc(db, "users", email));
118177
localStorage.removeItem("user");
119-
return res(ctx.status(200), ctx.json({ success: true }), ctx.json({ message: "로그아웃에 성공하였습니다." }));
178+
return res(ctx.status(200), ctx.json({ success: true }), ctx.json({ message: "회원탈퇴에 성공하였습니다." }));
120179
} catch (error) {
121-
return res(ctx.status(400), ctx.json({ success: false }), ctx.json({ message: "로그아웃에 실패하였습니다." }));
180+
return res(ctx.status(400), ctx.json({ success: false }), ctx.json({ message: "회원탈퇴에 실패하였습니다." }));
181+
}
182+
}),
183+
// 게시글 작성 mocking API
184+
rest.post("http://localhost:3000/postEditor", async (req, res, ctx) => {
185+
const user = localStorage.getItem("user");
186+
const { email } = JSON.parse(user as string);
187+
const { content, dynamicKeywords, staticKeywords, seasonKeywords, weatherKeywords, uploadedImageUrls } =
188+
await req.json();
189+
try {
190+
const imagesArr = [];
191+
for (const file of uploadedImageUrls) {
192+
const response = await fetch(file);
193+
const blob = await response.blob();
194+
const uniqueId = uuidv4();
195+
const storageRef = ref(storage, `postImages/${uniqueId}`);
196+
await uploadBytes(storageRef, blob);
197+
const url = await getDownloadURL(storageRef);
198+
imagesArr.push({ url });
199+
}
200+
// const docRef = doc(collection(db, "posts", email, "articles"));
201+
// await setDoc(docRef, {
202+
// email,
203+
// content,
204+
// dynamicKeywords,
205+
// staticKeywords,
206+
// seasonKeywords,
207+
// weatherKeywords,
208+
// images: imagesArr,
209+
// createdAt: serverTimestamp(),
210+
// });
211+
await addDoc(collection(db, "posts"), {
212+
email,
213+
content,
214+
dynamicKeywords,
215+
staticKeywords,
216+
seasonKeywords,
217+
weatherKeywords,
218+
images: imagesArr,
219+
createdAt: serverTimestamp(),
220+
});
221+
222+
return res(
223+
ctx.status(200),
224+
ctx.set("Content-Type", "multipart/form-data"),
225+
ctx.json({ success: true }),
226+
ctx.json({ message: "게시글 작성에 성공하였습니다." }),
227+
);
228+
} catch (error) {
229+
return res(ctx.status(400), ctx.json({ success: false }), ctx.json({ message: "게시글 작성에 실패하였습니다." }));
230+
}
231+
}),
232+
//날씨 정보 불러오기 mocking API
233+
// rest.post("http://localhost:3000/weather", async (req, res, ctx) => {
234+
// const { lat, lon } = await req.json();
235+
// try {
236+
// // 현재 위치의 날씨 정보를 불러옵니다.
237+
// // 날씨 정보가 있을 때,
238+
// const response = await axios.get(
239+
// `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current_weather=true&timezone=auto`,
240+
// );
241+
// const curTemperature = response.data.current_weather.temperature;
242+
// const weatherCode = response.data.current_weather.weathercode;
243+
// return res(
244+
// ctx.status(200),
245+
// ctx.json({ success: true, message: "날씨 정보를 불러오는데 성공하였습니다.", curTemperature, weatherCode }),
246+
// );
247+
// } catch (error) {
248+
// // 현재 위치의 날씨 정보를 불러오지 못했을 경우, 서울의 날씨 정보를 불러옵니다.
249+
// const response = await axios.get(
250+
// `https://api.open-meteo.com/v1/forecast?latitude=37.566&longitude=126.9784&current_weather=true&timezone=auto`,
251+
// );
252+
// const curTemperature = response.data.current_weather.temperature;
253+
// const weatherCode = response.data.current_weather.weathercode;
254+
// return res(
255+
// ctx.status(400),
256+
// ctx.json({ success: false, message: "날씨 정보를 불러오는데 실패하였습니다.", curTemperature, weatherCode }),
257+
// );
258+
// }
259+
// }),
260+
rest.post("http://localhost:3000/weather", async (req, res, ctx) => {
261+
const { lat, lon } = await req.json();
262+
try {
263+
// 현재 위치의 날씨 정보를 불러옵니다.
264+
const response = await axios.get(
265+
`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current_weather=true&timezone=auto`,
266+
);
267+
const curTemperature = response.data.current_weather.temperature;
268+
const weatherCode = response.data.current_weather.weathercode;
269+
return res(
270+
ctx.status(200),
271+
ctx.json({ success: true, message: "날씨 정보를 불러오는데 성공하였습니다.", curTemperature, weatherCode }),
272+
);
273+
} catch (error) {
274+
return res(ctx.status(400), ctx.json({ success: false, message: "날씨 정보를 불러오는데 실패하였습니다." }));
122275
}
123276
}),
277+
// 게시글 불러오기 mocking API (한명의 유저가 작성한 게시글)
278+
// axios.get(`/posts?email=${email}`)
279+
rest.get(`http://localhost:3000/userPosts`, async (req, res, ctx) => {
280+
const email = req.params.email;
281+
try {
282+
const userPosts: object[] = [];
283+
284+
const q = query(collection(db, "posts"), where("email", "==", email));
285+
const querySnapshot = await getDocs(q);
286+
querySnapshot.forEach((doc) => {
287+
userPosts.push(doc.data());
288+
});
289+
return res(
290+
ctx.status(200),
291+
ctx.json({ success: true, message: "게시글을 불러오는데 성공하였습니다.", userPosts }),
292+
);
293+
} catch (error) {
294+
return res(ctx.status(400), ctx.json({ success: false, message: "게시글을 불러오는데 실패하였습니다." }));
295+
}
296+
}),
297+
// 날씨 추천 게시글 불러오기 mocking API (모든 유저가 작성한 게시글 중에서 seasonKeywords와 weatherKeywords 일치하는 게시글)
298+
// axios.get(`/posts?seasonKeywords=${seasonKeywords}&weatherKeywords=${weatherKeywords}`)
299+
// Url은 위와 같이 작성해주시면 될 것 같습니다.
300+
// ft/main 브랜치를 기준으로 설명드리면,
301+
// 현재 날씨 정보는 Weather.tsx에서 불러오고 있기 떄문에,
302+
// ${}안에 들어가는 날씨 키워드와 계절 키워드는 Weather.tsx에서 불러온 날씨 정보를 기준으로 작성하시면 될 것 같습니다.
303+
// 다만, 날씨 추천 게시글 불러오기 api가 main 페이지에서 호출되어야 하므로,
304+
// 1. Weather.tsx에서 날씨 정보 호출
305+
// 2. 날씨 정보를 토대로, 날씨 키워드와 계절 키워드를 설정하고,
306+
// 3. 이때, 날씨 키워드와 계절 키워드는 main페이지에서 사용되어야 하므로, Recoil을 사용하여 전역 상태로 만들어 주시고,
307+
// 4. Main 페이지에서 위의 axios 요청에 필요한 키워드를 전역 상태로 만들어둔 키워드를 사용하여 요청하시면 될 것 같습니다.
308+
rest.get(`http://localhost:3000/posts`, async (req, res, ctx) => {
309+
const seasonKeywords = req.url.searchParams.get("seasonKeywords");
310+
const weatherKeywords = req.url.searchParams.get("weatherKeywords");
311+
312+
try {
313+
const posts: object[] = [];
314+
const seasonq = query(
315+
collection(db, "posts"),
316+
where("seasonKeywords", "==", [seasonKeywords]),
317+
where("weatherKeywords", "==", [weatherKeywords]),
318+
);
319+
const seasonQuerySnapshot = await getDocs(seasonq);
320+
seasonQuerySnapshot.forEach((doc) => {
321+
const data = doc.data();
322+
posts.push(data);
323+
});
324+
// const weatherq = query(collection(db, "posts"), where("weatherKeywords", "==", weatherKeywords));
325+
// const weatherQuerySnapshot = await getDocs(weatherq);
326+
// weatherQuerySnapshot.forEach((doc) => {
327+
// posts.push(doc.data());
328+
// });
329+
return res(ctx.status(200), ctx.json({ success: true, message: "게시글을 불러오는데 성공하였습니다.", posts }));
330+
} catch (error) {
331+
return res(ctx.status(400), ctx.json({ success: false, message: "게시글을 불러오는데 실패하였습니다람쥐." }));
332+
}
333+
}),
334+
// 게시글 불러오기 mocking API (키워드에 따른 게시글 조회)
335+
// rest.get(`http://localhost:3000/posts/`, async (req, res, ctx) => {
336+
// // url에 담긴 키워드를 가져옵니다.
337+
// const { staticKeywords, seasonKeywords, weatherKeywords } = req.params;
338+
// try {
339+
// // dynamicKeywords, staticKeywords, seasonKeywords, weatherKeywords가 여러개일 경우, 각각의 키워드를 배열로 만듭니다.
340+
// // dynamicKeywords가 존재할 경우, dynamicKeywords에 해당하는 게시글을 불러옵니다.
341+
// if (staticKeywords) {
342+
// // staticKeywords가 존재할 경우, staticKeywords에 해당하는 게시글을 불러옵니다.
343+
// const posts: object[] = [];
344+
345+
// for (const keywords of staticKeywords) {
346+
// const q = query(collection(db, "posts"), where("staticKeywords", "==", keywords));
347+
// const querySnapshot = await getDocs(q);
348+
// querySnapshot.forEach((doc) => {
349+
// posts.push(doc.data());
350+
// });
351+
// }
352+
// // posts에 담긴 게시글 중에서 seasonKeywords와 weatherKeywords가 일치하는 게시글만 불러옵니다.
353+
// const filteredPosts = posts.filter((post) => {
354+
// return post.seasonKeywords === seasonKeywords && post.weatherKeywords === weatherKeywords;
355+
// });
356+
// return res(
357+
// ctx.status(200),
358+
// ctx.json({ success: true, message: "게시글을 불러오는데 성공하였습니다.", filteredPosts }),
359+
// );
360+
// }
361+
// } catch (error) {
362+
// return res(ctx.status(400), ctx.json({ success: false, message: "게시글을 불러오는데 실패하였습니다." }));
363+
// }
364+
// }),
365+
// 게시글 불러오기 mocking API (팔로잉한 유저가 작성한 게시글)
124366
];

next.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
3+
images: {
4+
domains: ['firebasestorage.googleapis.com','mblogthumb-phinf.pstatic.net','blog.kakaocdn.net'],
5+
},
36
reactStrictMode: true,
47
};
58

0 commit comments

Comments
 (0)