메인 UI
로그인 전 (JWT 미존재)
로그인 후
우선 메인 페이지에 로그인 하기 전에 회원가입에서 Hash와 salt를 활용한 pw암호화를 진행하였습니다. (#2 회원가입 참조)
<div class="col-lg-8 col-4 mx-auto bg-white">
<div class="m-2 text-center">
<a href="/">
<img src={logo} class="img-fluid" alt="내일 지구가 끝나더라도 나는 오늘 밤 최고의 술자리를 가지겠어" width="300" />
</a>
</div>
<div class="p-2 ">
<div class="border rounded m-3 p-3">
<form onSubmit={submitHandler}>
<label class="p-3 font-500">ID</label>
<input type="text" class="form-control form-control-lg mb-3 rounded-pill"
placeholder="아이디를 입력하세요." value={id} onChange={idHandler}></input>
<label class="p-3 font-500">Password</label>
<input type="password" class="form-control form-control-lg rounded-pill"
placeholder="비밀번호를 입력하세요." value={pw} onChange={pwHandler}></input>
<button class="btn btn-lg press_btn mt-5 d-grid gap-2 col-11 mx-auto" type="submit">
LOGIN
</button>
</form>
<div class="text-center pt-4">
<p class="m-3 text-secondary font-500">
아직 계정이 없으신가요?{" "}
<Link class="text-dark font-500" to="/Signup">
회원가입
</Link>
</p>
</div>
</div>
</div>
</div>
* idHandler와 pwHandler에 대한 코드는 #2 회원가입을 참고해주세요!
[클라이언트]
async function submitHandler(e) {
e.preventDefault();
// 데이터베이스의 암호화된 비밀번호를 찾아 대입할 변수 생성
let SALT_pw;
// 폼에 입력한 ID를 세션 스토리지에 저장 (GBN 회원정보 띄울용)
sessionStorage.setItem("ID", id);
try {
await axios.get("api/pw").then((응답) => {
console.log("입력한 아이디", id);
{
for (let i = 0; i < 응답.data.length; i++) {
// 아이디에 해당하는 암호화된 비밀번호를 가져와 SALT_pw에 대입
// 닉네임을 기입한 유저라면 닉네임도 함께 가져와 NickName에 대입
응답.data[i].아이디 == id && (SALT_pw = 응답.data[i].패스워드);
응답.data[i].아이디 == id && (NickName = 응답.data[i].닉네임);
}
}
// 닉네임을 찾았다면 닉네임도 sessionStorage에 저장
sessionStorage.setItem("Nickname", NickName);
// bcrypt.compare를 위해 암호화 비밀번호와 폼입력 비밀번호 둘 모두 전달
let R_body = {
id: id,
pw: SALT_pw,
form_pw: pw,
};
// 서버로부터 온 응답의 경우에 따른 경고창
axios.post("api/login", R_body).then((res) => {
if (res.data == "아이디미존재") {
alert("아이디를 잘못 입력하셨습니다.");
} else if (res.data == "비번미존재") {
alert("비밀번호가 틀렸습니다.");
} else {
// 세션에 토큰 저장
// res.data = 서버로 부터 전송된 Json Web Token
sessionStorage.setItem("JWT", res.data);
// 토큰이 있어야만 main에 접근가능 (즉, 성공적인 로그인)
{
sessionStorage.JWT != null && navigate("/main");
}
location.reload();
}
});
});
} catch (err) {
console.log(err);
}
}
[서버]
// 1. 데이터베이스의 정보를 클라이언트에 전송 API
app.get("/api/pw", function (요청, 응답) {
db.collection("login")
.find()
.toArray(function (에러, 결과) {
응답.json(결과);
});
});
// 2. 로그인 토큰 발급 API
app.post("/api/login", function (요청, 응답) {
db.collection("login").findOne({ 아이디: 요청.body.id }, function (에러, 아이디결과) {
// 에러처리
if (에러) return console.log(에러);
// 아이디 검사
if (아이디결과) {
db.collection("login").findOne({ 패스워드: 요청.body.pw }, function (에러, 비번결과) {
if (비번결과) {
// 폼 입력 비번을 암호화 된 비밀번호와 Compare
bcrypt.compare(요청.body.form_pw, 요청.body.pw).then((result) => {
if (result) {
jwt.sign({ foo: "bar" }, "secret-key", { expiresIn: "1d" }, (err, token) => {
if (err) res.status(400).json({ error: "에러요" });
// 생성된 토큰 전송
응답.json(token);
});
} else 응답.json("비번미존재");
});
}
});
} else {
응답.json("아이디미존재");
}
});
});
로그인 형식에 맞춰 아이디와 비밀번호를 입력을 마친 후 로그인 버튼을 눌렀을 때
두 가지 API가 동작을 합니다.
첫번째로는 데이터베이스의 정보를 클라이언트에 전송하는 API이고
두번째는 회원가입을 통해 데이터베이스에 성공적으로 저장된 회원정보가 일치할시 JWT를 발급하는 API입니다.
다만, 여기에서 아쉬운 점은 for로 회원 정보를 탐색하는 비용을 생각하지 않고 개발을 한 점과 로그인 세션유지관리를 하지 않았단 점, 또한 개인적인 생각으로 bcrypt.compare의 수행을 위해 두 비밀번호를 전달하는 과정이 매끄럽지 않다고 생각이든다..
헤더 UI
헤더 코드
const ID = sessionStorage.getItem("ID"); const NickName = sessionStorage.getItem("Nickname"); const myJWT = sessionStorage.getItem("JWT"); const [address, setAddress] = useState(""); useEffect(() => { myJWT == null ? navigate("/") : navigate("/Main"); const URL = "https://geolocation-db.com/json/2725d960-5eef-11ed-9b62-857a2b26943e"; fetch(URL) .then((res) => res.json()) .then((data) => setAddress(data)); }, []); --------------------------- 하단 랜더링 부분------------------------------- {/* 로그인 시 추가 내용 */} {myJWT != null && ( <> <ul class="navbar-nav ms-auto mb-2 mb-lg-0"> <li className="nav-item me-4 ms-auto mt-1"> <span className="align-middle"> 현재위치 :{" "} <b> {address?.city}({address?.country_code}) </b> </span> </li> <li className="nav-item me-4 ms-auto mt-1"> <Link to="/Mypage" style={{ textDecoration: "none", color: "Black " }}> {NickName ? ( <span className="align-middle "> 환영합니다💖! <b>{NickName}</b>님! </span> ) : ( <span className="align-middle "> 환영합니다💖! <b>{ID}</b>님! </span> )} </Link> </li> <li className="nav-item ms-auto"> <button className="btn btn-secondary press_btn me-2 mt-1"> <Link to="/Mypage" style={{ textDecoration: "none", color: "white " }}> 마이페이지 </Link> </button> </li> <li className="nav-item ms-auto"> <button className="btn btn-secondary press_btn me-2 mt-1" onClick={(e) => { e.preventDefault(); sessionStorage.clear(); navigate("/"); location.reload(); }} 로그아웃 </button> </li> </ul> </> )}
헤더는 서비스를 이용 전 로그인 창의 깔끔함을 유지하기 위해 로그인을 한 후에 보이도록 했습니다.
우선 우측에 현재위치 같은 경우
useEffect(() => {
myJWT == null ? navigate("/") : navigate("/Main");
const URL = "https://geolocation-db.com/json/2725d960-5eef-11ed-9b62-857a2b26943e";
fetch(URL)
.then((res) => res.json())
.then((data) => setAddress(data));
}, []);
useEffect를 활용하여 moubt시 1회씩 geolocation를 사용해 사용자의 위치를 받아오고 또한 세션스토리지에 저장되어 있는 ID, NickName, JWT 중 JWT의 존재 여부를 통하여 정상적인 접근으로 로그인을 하였는지 검사합니다.
여기에서 JWT가 존재하지 않는다면 다시 login창으로 보냅니다.
3항 연산자를 활용하여 sesstionStorage에 저장되어 있는 NickName이 있다면 NickName으로 NickName이 존재하지 않는다면 본인이 입력한 ID가 뜨도록 했습니다.
마이페이지 버튼은 코드에 나와있는 그대로 마이페이지로 이동하도록 Link를 설정 해놓았고
로그아웃 버튼은 로그아웃을 클릭 했을시에 session에 저장되어있던 ID, NickName, "JWT"를 모두 지워 JWT가 존재 하지 않으므로 로그인창으로 이동하도록 하였습니다.
서비스 메인 UI
우선 대표적인 캐러셀이 두 개 보이는 것을 볼 수 있는데, 우선 큰 캐러셀 (상단) 먼저 설명하도록 하겠습니다.
react-slick-slider 라이브러리를 활용하여 구현하였습니다.
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
기본적인 setting을 마친 후에
let settings = {
dots: true,
infinite: true,
speed: 400,
slidesToShow: 3,
slidesToScroll: 1,
arrows: true,
cssEase: "linear",
};
<Slider {...settings} className="slider_center" dotsClass="test-css">
<div className="card-wrapper">
<div className="card">
<div className="card-image">
<img src={"/assets/0/0.jpg"} />
</div>
<ul className="social-icons">
<li>
<Link to={"/detail/0"}>
<p className=" fa fa-facebook">마시는 것이 힘이다</p>
</Link>
</li>
</ul>
<div className="details">
<h2>
소주 <span className="job-title">평균가격 4,500원 | 도수 16</span>
</h2>
</div>
</div>
</div>
// ... 타 주류 생략
</Slider>
캐러셀의 각 주류에 해당하는 것에 문구와, 정보들을 기입 하였습니다.
여기서 img src가 /0/0 으로 돼있는 이유는 추후 detail 페이지에서 usePrams를 사용하여 각 주류의 위치에 맞는 페이지를 작성하였기 때문입니다.
그렇게하여 Link를 활용하여 명언을 클릭시 해당하는 detail 페이지로 이동하게 됩니다.
하단 캐러셀은 SimpleImageSlider 라이브러리를 활용하여 구현하였습니다.
import SimpleImageSlider from "react-simple-image-slider";
<div className="col-lg-5 mx-auto">
<div class="container mt-2 p-1 rounded shadow-lg col-12">
<h2 class="m-3 text-center text-light">
<strong>주간베스트 안주 🍽</strong>
</h2>
</div>
<div class="pt-3 mx-auto testBOX ">
<SimpleImageSlider width={500} height={350}
images={images} showBullets={true} showNavs={true} autoPlay={true} autoPlayDelay={2.0} />
</div>
</div>
images에 해당하는 변수에 미리 public/asset에 저장해둔 사진들을 넣어 대표 사진을 반영 하였는데, 이 부분은 추후 데이터베이스 찜목록에 존재하는 찜수를 계산하여 Rank에 따른 순으로 이미지를 기입하고 싶은 생각이 개발하며 굉장한 아쉬움과 함께 많이 떠올랐습니다.
또 한 가지로 여기서 autoPlay를 true로 설정하여 배달의 민족의 배너 광고처럼 여러 안주들을 훑고 지나갈 수 있도록 세팅하였습니다.
React socket 통신 관련하여서는 양이 방대한 이유로 #4에서 다루겠습니다
푸터 UI
푸터 코드
<div className="Container mt-5"> <div className="UpBtn" onClick={clickToTop}> <IoIosArrowUp className="UpIcon" /> </div<> </div> <div> <span className="FooterText"></span> <span className="FooterText">경기도 성남시 분당구 대왕판교로 644번길 12 (우)13494 12, Daewangpangyo-ro 644beon-gil, Bundang-gu, Seongnam-si, Gyeonggi-do, Korea (13494). </span> </div>
const clickToTop = () => {
window.scrollTo(0, 0);
};
window 객체의 scrollTo 메소드를 활용하여 제일 상단으로 이동하도록 하였습니다. (smooth)