Github Frontend : https://github.com/nonjee888/fe_clone1jo
Frontend : 김보미 노은지 임다은
Backend : 박성수 정민우 전대훈
기능 진행
네이버 로그인 : 완료 (로그인상태확인 완료)⭕️
로그아웃 버튼 만들기(로그인시만 보임)+ 로그아웃 기능 :완료 ⭕️
상세페이지 정보 불러 와서 붙이기 : 완료 ⭕️
상세페이지 찜 하기 버튼 : 완료 ⭕️
무비차트 정보 불러 와서 붙이기 : 완료 ⭕️
카카오 로그인 : 완료 ⭕️
무비차트 현재상영중 체크박스 선택시 상영중인 영화만 보이기 : 완료 ⭕️
마이페이지에 정보 불러 와서 붙이기 : 완료 ⭕️
⭐️⭐️예매하기 : ( 월요일 새벽부터 시작~수요일 저녁 까지 끝내기) : 다은 진행중 현재 좌석선택창
메인페이지
**https://swiperjs.com/react 리액트 슬라이더 패키지**
https://www.npmjs.com/package/react-player 리액트 플레이어 패키지
동영상 플레이어에 동영상 mute 해제, play&pause 버튼 구현 해보기
swiper slider에서 아이콘 바꾸기 한번에 5장씩 넘어가게 구현 해보기
예매페이지
- 영화 선택하면 선택한 영화정보를 state로 받기
//영화 선택 컴포넌트에서
const [movieTitle, setMovieTitle] = useState("");
<li><a onClick={()=>setMovieTitle("공조2-인터내셔날")}>
<span>공조2-인터내셔날</span>
</a></li>
- 선택한 정보로 state가 바뀔때마다 렌더링시키고 스토어state에 저장하는 리듀서로 dispatch해줌
//영화 선택 컴포넌트에서
useEffect(() => {
dispatch(choiceMovie(movieTitle))
}, [movieTitle]); //useEffect 되는 조건에(두번째 인자에) movieTitle이 들어간다.
//dispatch 된 booking 모듈에서
reducers: {
choiceMovie: (state , action) => {
state.Title = action.payload},
}
- 이렇게 영화 , 극장 , 날짜 다 스토어 state에 넣어줌
- 날짜까지 3개 전부 state에 들어가면 if (state.title / state.cinema / state.date ==! null ) → post요청
5)post 요청으로 받아온 response에 선택한정보의 상영시간정보들을 마지막칸 시간컴포넌트에 붙여주기→
6)시간 클릭하면 스토어 state로 들어가고 좌석선택버튼 활성화→ 좌석선택창 이동
에러
랜더링 무한루프 https://wnsdufdl.tistory.com/245
에러
Uncaught TypeError: Cannot read properties of undefined (reading 'includes')
useselecter 로 가져오는 state가 느리게 들어와서 includes가 돌아가며 첫렌더링시 오류
https://velog.io/@nemo/react-error-cannot-read-property - 해결!!
로그인페이지
네이버 로그인 과정 전체적인 흐름 이론
1️⃣네이버 로그인 버튼 클릭
버튼에 달아야 하는 링크
<https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=>{ } &redirect_uri={ }&state=state
client_id는 링크만들때 네이버가서 발급받고 client_id발급받을때 redirect_uri (콜백주소)를 등록할수있음
2️⃣버튼을 클릭해서 이동한 네이버에서 보내준 로그인링크안에서 로그인을 완료하면
redirect_uri 에 직접 입력한 주소로 사이트가
돌아오면서 뒤에 code가 붙어있게된다 (ex http://입력한주소?code=afhalakfhdakh )
(주소뒤에 ? 붙는건 정보 파라미터라 붙어도 주소자체가 변하는건 아님)
3️⃣저 code를 백에서 받아서 저걸 통해 네이버 로그인한사람의 네이버정보를 받고, 우리사이트의 토큰을
발급시켜서 클라이언트에 준다
4️⃣ 프론트는 그걸 로컬스토리지에 저장한다
구글링해서 본 다른사람들이 네이버로그인한 과정 (백,프론트가 어떤과정을 하냐에 따라 방법이 다들 조금씩 다름)
1)버튼눌러서 링크이동 //로그인다하면 코드붙여줄 콜백주소를 백엔드쪽 서버주소로 등록해둠
2)로그인다되면 백엔드쪽 서버주소로 code가 가서 주소에서code를 떼서 정보활용하고 토큰발급
3)백엔드에서 리다이렉트기능으로 클라이언트 주소를 (localhost:3000/main/?${토큰})으로 설정.(주소 뒤에?있어도 파라미터(정보값)이라 그냥 main으로 가짐)
4)사이트는 main으로 와지면서 주소에 토큰도있어서 프론트에서 주소에서 토큰꺼내서 로컬스토리지에 저장
사용자가 보는 사이트흐름 (로그인버튼 -> 네이버로그인창 -> 메인화면)
우리가 한 네이버로그인과정
1)버튼 눌러서 링크이동 // 콜백주소를 localhost:3000/loading/으로함(프론트한테 localhost:3000/loading/?code=…로 code가 옴)
2)코드가 우리한테만 있기 때문에 Get요청을 “서버주소+code” 주소에 요청하면 요청들어온 그 주소를통해 백에서 code를 받아서 사용후 토큰을 발급해 get요청 response에 토큰줌
3)response에서 토큰꺼내서 로컬스토리지에 저장
4)get함수가 끝나는 마지막에 window.location로 페이지 main으로 이동시켜줌
사용자가 보는 사이트흐름 (로그인버튼 -> 네이버로그인창 -> 로그인중페이지(/loading) -> 메인화면)
loading페이지(스피너페이지 /loading)를 만들어서 그걸 콜백주소로 등록하고 로그인완료하면
일단 콜백주소에 등록된 loading페이지에 와지고 페이지에선 useeffect를 사용해 get함수 실행시켜서
페이지 주소로 들어온 code를 백에 보내고 토큰받아서 저장하고 다하면 main으로 이동시켜주는 함수 넣어줌
//로그인 기능관련 작성코드는 저희 프로젝트 안에 loading 페이지와 user모듈에만 있습니다 //
로그인상태확인
app.js에서 useEffect될때마다 실행됨 → 페이지에서 모든활동을 할때마다 실행된다고 생각
app.js 파일에서
useEffect(() => {
if(localStorage.getItem("token") !== null){ // Storage 에 token 저장된 값이 있다면 is_Login 상태를 true로 바꿔주는 함수로 보냄
dispatch(loginCheck()) //app.js에서 뭔가실행될때마다 항상 로컬스토리지에 토큰이 있나 없나보고 state의 is_Login상태를 바꿔줌
}
},[])
모듈 user.js 파일에서
reducers: {
//로그인상태확인 리듀서
loginCheck: (state) => { //app.js에서 뭔가실행될때마다 항상 로컬스토리지에 토큰이 있나 없나보고 state의 is_Login상태를 바꿔줌
state.is_Login = true
},
},
새로고침을 해도 app.js에서 바로 useEffect하기 때문에 로컬스토리지에 토큰있으면 로그인상태 바로
true로 바꿔줌 //로그인상태 기본값은 false//
이제 페이지 어떤곳에서든 로그인상태를 확인할 필요가있을때(마이페이지,예매,로그아웃버튼보이기)
const 로그인상태체크 = useSelector((state) => state.user.is_Login)
해서 false면 로그아웃상태 true면 로그인상태
에러
토큰을 get하는 함수가 useEffect 에서 두번 dispatch 되는 현상
→useeffect가 액션함수 두번 호출
해결 : index.js 의 strictmode 주석처리. https://youngble.tistory.com/175
상세페이지
-크롤링된 영화줄거리 서버에서 받아서 뿌려줄때
글씨강조표시는 못한다해도 본문 줄바꿈은 어떻게 해줘야할지.. 크롤링데이터가 어떻게 올지 확인해보기
트러블 슈팅
→ BE에서 크롤링 시 HTML로 크롤링을 하면 “\n”이 삽입되어 크롤링 되어왔다.
이것을 활용하려면 디테일을 감싼 <p>의 부모 태그인 <div> 안에서 <p>태그에 style을 주거나 직접 <p>에 style을 주어서 white-space : pre-line 으로 지정해주면 “\n” 이 활성화되어 줄바꿈이 된다.
문제는 \n 뿐만 아니라 태그와 특수문자들이 포함되어서 자바스크립트 정규표현식을 이용해 태그를 삭제 혹은 띄어쓰기로 바꾸어줌
var newText = detail.detail.replace(/(<([^>]+)>)/gi, ""); //태그 제거
var tmp = newText.replace(/ /gi, " "); //공백 제거
var tmp2 = tmp.replace(/</gi, ""); //부등호(<) 제거
var tmp3 = tmp2.replace(/>/gi, ""); //부등호(>) 제거
무비차트 페이지
무비차트 페이지는 영화 상태값에 따라 분류를 달리 해 주어야 한다.
영화에 상태값 설정
status = 1 ( 상영 예정 영화 )
status = 2 ( 상영 중인 영화 )
status = 3 ( 상영 종료 영화 )
useState 이용해 initialState를 2로 주고 비교값을 만들어주고
삼항 연산자 사용해 상태값이 1, 2, 3 일때 해당하는 movies를 map으로 각각의 경우에 수에 따른 데이터 뿌려주기
import MovieCard from "./MovieCard";
import styled from "styled-components";
import { React, useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getMovies } from "../../redux/modules/movies";
const MovieListCard = () => {
const dispatch = useDispatch();
const { isLoading, error, movies } = useSelector((state) => state.movies);
const [Content, setContent] = useState(2);
const onChangeHandler = (e) => {
setContent(e.currentTarget.value);
};
const Status = [
{ status: 2, value: "현재 상영작만 보기" },
{ status: 1, value: "상영 예정작 보기" },
{ status: 3, value: "상영 종료작 보기" },
];
let Movienow = movies.filter((movies) => {
return movies.status === 2;
});
// console.log(Movienow);
let Moviebefore = movies.filter((movies) => {
return movies.status === 1;
});
// console.log(Moviebefore);
let Movieafter = movies.filter((movies) => {
return movies.status === 3;
});
// console.log(Movieafter);
useEffect(() => {
dispatch(getMovies());
}, [dispatch]);
if (isLoading) {
return <div>로딩 중....</div>;
}
if (error) {
return <div>{error.message}</div>;
}
// console.log(Content);
return (
<Container>
<Movielist>
<Listheader>
<h3>무비차트</h3>
</Listheader>
<Headercheckbox>
{/* <input type="checkbox"></input>
<p>현재 상영작만 보기</p> */}
<select onChange={onChangeHandler} value={Content}>
{Status.map((movies) => (
<option status={movies.status} value={movies.status}>
{movies.value}
</option>
))}
</select>
</Headercheckbox>
<Moviecardlist>
{Content == 2
? Movienow.map((movie) => {
return (
<MovieCard
movie={movie}
key={movie.id}
id={movie.id}
status={movie.status}
/>
);
})
: Content == 1
? Moviebefore.map((movie) => {
return (
<MovieCard
movie={movie}
key={movie.id}
id={movie.id}
status={movie.status}
/>
);
})
*: Movieafter.map((movie) => {
return (
<MovieCard
movie={movie*}
key={movie.id}
id={movie.id}
status={movie.status}
/>
);
})}
</Moviecardlist>
</Movielist>
</Container>
);
};
export default MovieListCard;
Trouble shooting 처음에 status가 key로 정해져있어서 status와 key를 비교 할 수가 없었음.
const Status =[
{key:movies.status===2, value:"현재 상영작만 보기"},
{key:movies.status===1, value:"상영 예정작 보기"},
{key:movies.status===3, value:"상영 종료작 보기"}
]
선언된 Movienow, Moviebefore, Movieafter가 status 상태에 따라서 다르게 보여졌으면 좋겠다고 생각하여 삼항연산자로 코드를 짜봄. 연속 삼항연산자로 작성하였다. 처음에 map함수가 나오지 않아서 무엇이 잘못인지 파악을 계속 못하였는데 잘못된 참조값때문이었다.
Movieafter.map((after) => { return ( <MovieCard after={after} key={after.id} id={after.id} status={after.status} />
key도 value값도 after라고 주어서 자식 컴포넌트에 잘못된 참조값을 전달하고 있었음. movie로 바꾸니 해결됨.
마이페이지
Get 요청으로 내가 본 영화, 내가 찜한 영화 목록을 가져오기
내가 찜한 영화 목록이 한번 누르면 +1, 두번째 누르면 다시 -1 이 되도록 Backend에서 셋팅 해둠. 그래서 찜 삭제시 마이페이지에서 사라짐.
내가 본 영화는 예매 후 내 영화 목록에 추가됨.
//찜하기
export const onLikePost = createAsyncThunk(
"like/onLikePost",
async (payload, thunkApI) => {
try {
const data = await instance.post(`/movie/like/${payload}`);
return data.data.data; //는 "like success"라는 상태값
} catch (error) {
return thunkApI.rejectWithValue(error);
}
}
);
export const movies = createSlice({
name: "movies", //components로 data보내주는 extra 리듀서에 정보를 저장하기 위해 initialState를 만들어줍니다.
initialState: {
movies: [],
detail: {
title: "",
titleEng: "",
img: "",
date: "",
director: "",
actor: "",
rate: "",
genderRate: "",
genre: "",
base: "",
detail: "",
status: 2,
},
like: {},
mylist: {},
error: null,
},
reducers: {},
[onLikePost.pending]: (state) => {
state.isLoading = true; // 네트워크 요청이 시작되면 로딩상태를 true로 변경합니다.
},
[onLikePost.fulfilled]: (state, action) => {
// console.log(action); //엑스트라 리듀서에 액션이 성공적으로 들어왔는지 확인
state.isLoading = false; // 네트워크 요청이 끝났으니, false로 변경합니다.
state.like = action.payload; // Store에 있는 movies에 서버에서 가져온 movies를 넣습니다.
// console.log(state.like);
},
[onLikePost.rejected]: (state, action) => {
state.isLoading = false; // 에러가 발생했지만, 네트워크 요청이 끝났으니, false로 변경합니다.
state.error = action.payload; // catch 된 error 객체를 state.error에 넣습니다.
},
찜하기 버튼을 눌렀을때는 찜 삭제, 눌리지 않았을 때는 찜하기 상태로 보이게 하기위해서 찜했을때 상태값을 “true” 와 아닐때 “false”라는 상태값을 지정해주었고 그 상태를 true라는 string값과 비교하여 삼항연산자로
true이면 찜삭제가, false면 찜하기가 보이도록 바꾸어줌.
const likebtn = like == "true"; // likebtn = 스토어에서 오는 like와 "true"를 비교해서 true, false를 반환
<Bookingbut
onClick={() => {
navigate("/bookmovie");
}}
>
예매하기
</Bookingbut>
<Likebut onClick={onLike}>
{likebtn ? "찜삭제" : "찜하기"}
</Likebut>
Trouble shooting 처음에 const likebtn = “true” 라고 하니 버튼이 바뀌지 않았다. like와 “true”를 비교하는 연산자를 써주자 변경되었다.
로그인 / 로그아웃 버튼 변경
로컬스토리지에 저장해두는 토큰을 삭제하는 방식으로 로그인, 로그아웃 버튼을 변경 하게끔 해주었다.
const logoutHandler = () => {
window.alert("로그아웃 하시겠습니까?");
localStorage.removeItem("token"); //로그아웃 버튼 누르면 로컬스토리지의 토큰을 지운다.
navigate("/");
window.location.reload(); //자동 새로고침을 위해 버튼을 누를때마다 리로드 해주도록 한다.
};
로그인 상태일 때만 My CGV 이동 가능하게 하기
My CGV 조회는 로그인 상태일 때만 가능했지만 페이지 이동은 가능해서 My CGV 페이지 내에서 로그아웃을 눌렀을때 메인페이지로 돌아가지 않았다. 로그인 상태일 때만 My CGV 페이지 내에 머무를 수 하기 위해서는
로그인 중을 판별 할 수 있는 상태 값이 필요하다. 이것을 token 유무로 판별 해 주었다.
const MyWatchedMovie = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { isLoading, error, mylist } = useSelector((state) => state.movies);
const login = localStorage.getItem("token"); // token 유무로 로그인 상태 판별하기
useEffect(() => {
login ? dispatch(getMypage()) : navigate("/login"); //로그인이 true이면 dispatch로 mylist를 불러오고 아니면 로그인페이지로 보낸다.
}, [dispatch]);
카카오 소셜 로그인
흐름도
순서
- 카카오 로그인을 요청하면 Redirect URI를 통해 파라미터에 인가코드를 받는다
- 파라미터에 온 인가코드를 저장하여 백엔드 서버로 보내준다.
- 백엔드 서버에서 카카오에 POST로 인가코드를 보내면 Response로 토큰을 받아서 프론트엔드에 전달하게되고,
- 클라이언트가 로그인을 하면 성공. 셋팅 한 대로 로컬 스토리지에 토큰이 저장되면 된다.
과정
- Kakao developers에서 카카오 소셜로그인을 선택하고 내 React 애플리케이션을 등록한다.
2.REST API키를 받고 Redirect URI에 카카오로부터 인가코드를 받을 URI를 등록한다. 나의 경우 카카오 로딩페이지로 등록. 프론트엔드에서 접근 가능한 주소여야 한다고 한다.
//KakaoLoading.jsximport React from "react";
import styled from "styled-components";
import { useDispatch } from "react-redux";
import { getKakao } from "../redux/modules/user";
import { useEffect } from "react";
const KakaoLoading = () => {
const dispatch = useDispatch();
const params = new URLSearchParams(window.location.search);//주소뒤의 ? 가 파라미터를 전달해준다는 뜻//?code=..이면 주소창이 전달해주는 파라미터의 이름은 code 이다.let code = params.get("code");
console.log(code);//주소창에서 localhost3000/loading/?code= .... 에서 code= "~~~" 가져오기
useEffect(() => {
//주소창에서 뗀 code를 토큰가져오는 함수에 보내줌
dispatch(getKakao(code));
}, []);
return (
3.로그인 버튼을 만든다.
//OAuth.jsconst REST_API_KEY = "Kakao에서 받은 REST API";
const REDIRECT_URI = "";//Redirect를 loading페이지로 함export const KAKAO_AUTH_URL = `https://kauth.kakao.com/oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code`;
- KAKAO_AUTH_URL을 import 해서 로그인 버튼에 붙여준다.
5.카카오 로그인 버튼 이미지 주소도 긁어와서 a태그 안에 붙여줌.
//카카오 로그인 미들웨어 //엑스트라리듀서 안씀
export const getKakao = createAsyncThunk(
"user/getKakao",
async (code, thunkAPI) => {
//주소창의 code 뽑아낸걸 payload로 받음try {
const data = await instance.get(`/auth/kakao?code=${code}`);//서버주소+코드정보 로 get요청을 보내면 response에 토큰을 받을수있다.const ACCESS_TOKEN = data.data.data;
localStorage.setItem("token", ACCESS_TOKEN);//로컬스토리지에 토큰저장
window.location.assign("/");//토큰 저장하면 자동으로 메인화면으로 이동return data;
} catch (error) {
return thunkAPI.rejectWithValue(error);
}
}
);
- 카카오 로그인 누르면 개인 정보 제공 동의하기를 누르고
- Get이 성공적이라면 로딩페이지에서 메인페이지로 들어가고 로컬스토리지에 토큰이 저장되어야한다.
에러코드 404 ,500
첫 타에 홈런을 칠리가 없지 무한 로딩이 이어졌다. 404 에러로 서버에서 찾을수 없는 페이지에 사용자가 접근하려 했을 때 받는 에러 메시지가 떴다. payload에는 code가 잘 전송이 되었는데도.
이것 저것 시도해 보기
- API 주소는 BE에서 작성한대로 "/auth/kakao" 로 잘 썼는지 확인해보기
- "/auth/kakao/"로 붙여보기도 함
- 서버 주소 끝에 "/" 가 붙어있어서 중복으로 "/"가 들어간건 아닌지 확인
- 카카오에서 받은 Rest API Key가 잘못된 것은 아닌지 확인
- Redirect URI 도 혹시모르니 확인
- useEffect로 미들웨어로 잘 dispatch 해주고 있는지 확인
- 마지막으로 BE분과 API가 맞는지 확인해보았고 현재 서버를 병합 중이시라서 404에러가 뜬것같다고 말씀하셨다.
- 그러고 500에러가 떴는데 정보는 잘 가졌지만 생일을 형변환하는 과정에서 알고리즘 과정에서 오류가났다.
- 이번엔 닉네임을 가져오는 과정에서 알고리즘상 오류로 500에러.
- 백엔드쪽 알고리즘을 손본 후에 정상적으로 로그인이 가능해짐!
해결)
프론트와 백에서 각각다른 REST IP와 redirect_uri 사용하고 있어서 수정해줌
백엔드쪽 알고리즘 수정
그외 해결한 문제 정리!
프론트)1 )Cannot read properties of undefined
리액트에서 가장 자주 보는 오류 메시지이다. 이 오류는 값이 정의되지 않아 읽을 수 없을 때 발생한다.
includes 로 배열을 돌리기 전에 값이 불러와지지 않아 오류 발생.
Uncaught TypeError: Cannot read properties of undefined (reading 'classList')
해결)
&& 연산자를 사용, 앞뒤로 false 값을 찾고, false가 없다면 뒤에 있는 값을 출력한다.
조건식에 false가 있는 경우 null이 되고 렌더링하지 않으며, 렌더링하지 않으니 오류도 출력되지 않는다.
CGV 클론 코딩을 마치며
처음 도전하게될 영화표 예매 기능이 어렵지만 재미있을 것 같아서 도전하게 된 CGV 웹사이트 클론 코딩. 영화표 예매 서비스 기능까지 도전 해 보고 싶었지만 앞으로의 실전 프로젝트 때 원활한 진행을 위해서 리덕스와 엑스트라 리듀서를 한번 더 복습하는 것도 좋을 것 같아서 다른 한 분이 예매 기능을 맡았고, 예매 기능을 제외한 다른 모든 기능을 맡게 되었다.
일단 스코프가 너무 많아서 뷰를 그리는데 금, 토, 일을 꼬박 쏟아부었다. 또한 100% 클론을 해내기에는 스코프가 너무 많았어서 처음에 영화 검색 기능을 빼고 진행하게 되었다.
처음 해보는 div박스 안에 div 박스를 넣고 정렬하고 margin 과 padding을 주는 것은 생각보다 많이 어려웠고 내 마음대로 되지 않았다. 반응형 웹페이지 여야하는데 갑자기 한 섹션은 고정이되어 브라우저 크기가 늘어날때도 혼자 구석에 붙어있는 등의 일이 있었으나 width값이 부모 div의 width와 달라서 생긴 일이었다. 도와주셨던 CSS 고수님에게 많이 배울 수 있었음. 어떻게 CSS적인 레이아웃 구조를 잡아야 하는지를 파악하고 새로운 CSS기능도 접할 수 있는 좋은 프로젝트였다.
CRUD 중 거의 U만 포함 되어있었지만 이 프로젝트에서 기능구현적으로 어려웠던 점은 처음으로 시도했던 카카오 소셜 로그인, 영화 status 값에 따라 드롭다운에서 선택했을때 영화가 분류되게 하는것이었다. 영화 status에 따라 다른 map을 그리고 연속 삼항연산자를 써본 것은 아주 좋은 경험이 된 듯 하다. 9할이 매니저님의 도움으로 이루어졌지만...
백엔드와의 두번째 협업에서 모두 열정적으로 과제에 임하는 팀원분들을 만나서 원활하고 빠른 진행이 가능했던 점이 만족스럽다. 전반적으로 완성도가 높은 결과물이 나와서 여태껏 진행했던 과제중에 가장 뿌듯했다. 그리고 잠을 제일 못 잔 한 주였다.
다음주에 실전 프로젝트가 시작되는데 아직도 내가 백프로 리덕스를 이해하진 못한 것 같아서 무섭다. 이제 기술 매니저님들의 도움도 없을 것이라서 괜히 리더지원한건가 싶기도 하다. 어떻게 될 지 모르겠지만 열심히는 해 봐야지.
'Sparta x 이노베이션 캠프 > 팀 프로젝트' 카테고리의 다른 글
실전 React 프로젝트 내돈내여 | 10주차 TIL : 웹어플리케이션 내돈내여 중간 발표 전 정리 (0) | 2022.10.08 |
---|---|
TIL : React 카카오 api 맵 예외처리, 카카오맵 중심좌표 이동시키기(Trouble Shooting, 코드리팩토링 ver.1) (0) | 2022.10.07 |
CGV 클론코딩 Trouble shooting (0) | 2022.09.15 |
7주차 : React로 CGV클론코딩. 카카오 소셜로그인 프론트엔드 (0) | 2022.09.13 |
6주차: React 미니프로젝트 "오늘도 무사히" (0) | 2022.09.12 |