| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 재귀
- 멀티캠퍼스IT부트캠프티
- 시간 복잡도
- 스레드
- LG유플러스 유레카 프론트엔드
- 부트캠프후기
- LG유플러스 유레카 3기 프론트엔드
- 백준
- 프론트엔드 비대면반
- 알고리즘
- zod
- 별찍기10
- 멀티캠퍼스IT부트캠프
- 2775번 문제
- 코딩
- git branch 협업
- 프로세스
- LG유플러스 유레카 프론트엔드 개발자
- 유레카 부트캠프
- 소수
- LG유플러스 유레카 부트캠프
- 정렬
- 브루트포스
- 웹시큐리티
- Java
- 프론트엔드
- 애자일
- Do it! 자료구조와 함께 배우는 알고리즘 입문
- tanstack query
- 자바
- Today
- Total
개발 일기
20251001 uplus clone site 다시 만들기2 본문
어제 만든 사이트에 로그인, 로그아웃 기능을 추가하고 휴대폰 리스트를 볼 수 있도록 구현했다.
백엔드 부분은 어제와 거의 동일해서 프론트단 위주의 코드를 위주로 정리해봤다.
LG 유플러스 클론 사이트 만들기
Login 기능 구현하기
1. login.js에 코드 추가
login.js에 다음과 같은 코드를 추가했다.
document.addEventListener("DOMContentLoaded", () => {
document.body.addEventListener("click", e => {
if (e.target.classList.contains("login-btn") && e.target.classList.contains("upid")) {
e.preventDefault();
navigateTo("/upid");
}
});
});
코드를 분석하면 다음과 같다.
1. 페이지의 HTML이 전부 파싱되어 DOM 트리가 준비되면 안의 콜백을 실행한다. DOM 요소들이 있어야만 작동하는 스크립트를 안전하게 실행하도록 보장한다.
2. body에 클릭 이벤트 리스너를 하나 달아 이벤트 위임 방식으로 처리한다. 여러 개 버튼에 각각 리스너를 붙이지 않고 한 곳에서 처리하므로 성능과 관리 측면에서 유리하다.
3. 클릭한 실제 요소(e.target)가 두 개의 클래스(login-btn과 upid)를 모두 가지고 있으면 아래 동작을 실행
4. <a>같은 기본 동작을 가지고 있으면 그 기본 동작을 막고 navigateTo("/upid")를 실행
2. router.js에 path, view 추가
{ path: "/upid", view: renderUpid },
3. js/upid.js 생성 및 index.html 링크 코드 추가
// U+ID 로그인 화면
function renderUpid() {
return `
<div class="upid-card">
<h2>U+ID 로그인</h2>
<form id="upidForm">
<div class="form-group">
<label for="upid">아이디</label>
<input type="text" id="upid" name="upid" placeholder="아이디 입력" required>
</div>
<div class="form-group">
<label for="password">비밀번호</label>
<input type="password" id="password" name="password" placeholder="비밀번호 입력" required>
</div>
<button type="submit" class="btn-primary" >로그인</button>
</form>
<div class="links">
<a href="#">아이디/비밀번호 찾기</a> |
<a href="/signup" data-link>회원가입</a>
</div>
<div id="messageArea" style="margin-top: 1rem; text-align: center;"></div>
</div>
`;
}
만들어진 html을 그대로 사용했고 /upid 경로로 이동하면 위의 함수가 실행되고 렌더링된다.
CSS 코드
:root {
--lg-magenta: #e6007e;
--lg-gradient: linear-gradient(135deg, #e6007e, #ff1a96);
--lg-shadow: 0 8px 25px rgba(230, 0, 126, 0.25);
}
.upid-card {
background: #fff;
border-radius: 16px;
box-shadow: 0 8px 25px rgba(0,0,0,0.08);
max-width: 420px;
margin: 3rem auto;
padding: 2rem;
border: 1px solid rgba(230, 0, 126, 0.1);
text-align: center;
}
.upid-card h2 {
font-size: 1.6rem;
font-weight: 800;
margin-bottom: 1.5rem;
background: var(--lg-gradient);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.upid-card .form-group {
margin-bottom: 1.2rem;
text-align: left;
}
.upid-card .form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
font-size: 0.95rem;
color: #2c3e50;
}
.upid-card .form-group input {
width: 100%;
padding: 0.9rem 1rem;
border: 2px solid #e5e7eb;
border-radius: 10px;
font-size: 1rem;
background: #fafbfc;
transition: all 0.25s ease;
}
.upid-card .form-group input:focus {
border-color: var(--lg-magenta);
outline: none;
box-shadow: 0 0 0 3px rgba(230, 0, 126, 0.1);
}
.upid-card .btn-primary {
width: 100%;
padding: 0.9rem;
border: none;
border-radius: 12px;
background: var(--lg-gradient);
color: #fff;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
box-shadow: var(--lg-shadow);
transition: all 0.25s ease;
}
.upid-card .btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 12px 35px rgba(230, 0, 126, 0.35);
}
.upid-card .links {
margin-top: 1.5rem;
font-size: 0.9rem;
color: #555;
}
.upid-card .links a {
color: var(--lg-magenta);
font-weight: 600;
text-decoration: none;
}
.upid-card .links a:hover {
text-decoration: underline;
}
upid.js와 upid.css 추가
<script src="js/upid.js"></script>
<link rel="stylesheet" href="css/upid.css">
4. 로그인 버튼 누르면 fetch 요청이 가도록 처리
upid.js 코드 수정
// 이벤트 핸들러 등록
function attachUpidFormHandler() {
const form = document.getElementById("upidForm");
if (form) {
form.addEventListener("submit", async e => {
e.preventDefault();
//alert("U+ID 로그인 시도!");
// 실제 로그인 처리 로직 추가
// 서버로 POST 요청 전송
// 폼 데이터 수집
const formData = new FormData(event.target);
const id = formData.get('upid');
const pw = formData.get('password');
console.log(id,pw);
const response = await fetch('http://localhost:8080/upidLogin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
id,
pw
})
});
const result = await response.json();
if (result.msg=="success") {
// 성공 처리
showSuccessMessage(messageArea, 'login이 완료되었습니다!');
// 3초 후 홈 페이지로 이동
setTimeout(() => {
navigateTo('/');
}, 3000);
} else {
// 서버 에러 처리
showErrorMessage(messageArea, result.msg || 'login에 실패했습니다.');
}
});
}
}
// DOM 변경 감지 → 핸들러 부착
function setupUpidObserver() {
const observer = new MutationObserver(() => {
const form = document.getElementById("upidForm");
if (form && !form.hasAttribute("data-handler-attached")) {
attachUpidFormHandler();
form.setAttribute("data-handler-attached", "true");
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", setupUpidObserver);
} else {
setupUpidObserver();
}
폼데이터 id, pw를 POST 형식으로 서버에 보낸다.
5. 백엔드 구현
어제 구현한 서버와 크게 다르지 않다. 아래에서 위로 올라가면서 짜는게 더 직관적인 느낌
- MemberDao.java
package com.ureca.web.model.dao;
import org.apache.ibatis.annotations.Mapper;
import com.ureca.web.model.dto.Member;
@Mapper
public interface MemberDao {
public void registerMember(Member member) ;
public Member upidLogin(Member m); // 추가
}
Member를 리턴하고 Member를 받는 메서드 선언
- member.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ureca.web.model.dao.MemberDao">
<insert id="registerMember" parameterType="Member" >
INSERT INTO member (id,pw,email) VALUES (#{id}, #{pw}, #{email})
</insert>
// 추가된 코드
<select id="upidLogin" parameterType="Member" resultType="Member">
SELECT no, email FROM member WHERE id=#{id} and pw=#{pw}
</select>
</mapper>
registerMember와 동일하고 resultType만 void가 아니기 때문에 추가되었다.
- MemberService.java
public Member upidLogin(Member m) {
return memberDao.upidLogin(m);
}
위임만 한다.
- LoginController.java
package com.ureca.web.controller;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.ureca.web.model.UplusException;
import com.ureca.web.model.dto.Member;
import com.ureca.web.model.service.MemberService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
@RestController
public class LoginController {
@Autowired
MemberService memberService;
@PostMapping("/upidLogin")
public Map<String, Object> upidLogin(@RequestBody Member m, HttpServletRequest request){
System.out.println(m);
Map<String, Object> response=new HashMap();
try {
Member mem=memberService.upidLogin(m);
System.out.println(mem);
if(mem!=null) {
HttpSession session=request.getSession();
session.setAttribute("mem", mem);
response.put("msg", "success");
}else {
response.put("msg", "id와 pw를 확인해주세요");
}
}catch(Exception e) {
response.put("msg", e.getMessage());
//e.printStackTrace();
}
return response;
}
}
6. sessionStorage 사용
upid.js에 코드 추가
sessionStorage에 id를 저장
sessionStorage.setItem("id",id);
로그인 후 로그아웃 버튼 만들기
index.html에 버튼 자리 만들기
<span id="logoutBtn"></span>
index.js에 코드 수정 sessionStorage에 id가 있으면 로그아웃 버튼 보이게 처리
if(sessionStorage.getItem("id")){
console.log("logined");
const logoutBtn = document.getElementById("logoutBtn");
// 내용과 스타일을 버튼처럼 바꿔주기
logoutBtn.innerText = "로그아웃";
logoutBtn.classList.add("btn", "btn-danger");
logoutBtn.style.cursor = "pointer"; // 마우스 올리면 버튼처럼
}
실행 결과


Logout 기능 구현하기
1. 로그아웃 버튼을 눌렀을 때 처리
버튼을 누르면 end-point로 upidLogout 요청이 들어가게 하고 sessionStorage에 있는 id를 삭제한 뒤 페이지를 리로드한다.
// '로그아웃' 버튼 클릭 이벤트
const logoutBtn = document.querySelector('#logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', function(event) {
fetch('http://localhost:8080/upidLogout');
sessionStorage.removeItem("id");
location.href="/";
});
}
2. 백엔드 세션 무효화 처리
- LoginController.java
세션을 무효화한다.
@GetMapping("upidLogout")
public String upidLogout(HttpServletRequest request) {
HttpSession session=request.getSession(false);
if(session!=null) {
session.invalidate();
return null;
}else {
//침해 대응 코드
return "Get Out~!";
}
}
실행 결과

휴대폰 리스트 보기
1. html 휴대폰 링크 href를 수정 후 라우터 path 추가
{ path: "/phone", view: renderPhone },
2. js/phone.js 생성 후 html에 링크 추가
// phone 화면
function renderPhone() {
const phones = [
{
title: 'Special title treatment',
price: 100000,
img: 'https://www.lguplus.com/static/pc-contents/images/prdv/20250912-090008-227-4euoWeLA.png',
link: '#'
},
{
title: 'Special title treatment',
price: 100000,
img: 'https://www.lguplus.com/static/pc-contents/images/prdv/20250912-090008-227-4euoWeLA.png',
link: '#'
},
{
title: 'Special title treatment',
price: 100000,
img: 'https://www.lguplus.com/static/pc-contents/images/prdv/20250912-090008-227-4euoWeLA.png',
link: '#'
},
{
title: 'Special title treatment',
price: 100000,
img: 'https://www.lguplus.com/static/pc-contents/images/prdv/20250912-090008-227-4euoWeLA.png',
link: '#'
},
{
title: 'Special title treatment',
price: 100000,
img: 'https://www.lguplus.com/static/pc-contents/images/prdv/20250912-090008-227-4euoWeLA.png',
link: '#'
},
{
title: 'Special title treatment',
price: 100000,
img: 'https://www.lguplus.com/static/pc-contents/images/prdv/20250912-090008-227-4euoWeLA.png',
link: '#'
}
];
// 카드 아이템 템플릿
const cardItem = (item) => {
const title = item.title ?? 'Untitled';
const price = item.price ?? 0;
const img = item.img ?? 'https://via.placeholder.com/480x600?text=No+Image';
const link = item.link ?? '#';
return `
<div class="col-12 col-sm-6 col-md-4 col-lg-3">
<div class="card h-100">
<img class="card-img-top" src="${img}" alt="${title}">
<div class="card-body">
<h5 class="card-title">${title}</h5>
<p class="card-text">${price}</p>
<a href="${link}" class="btn btn-primary">Go somewhere</a>
</div>
</div>
</div>
`;
};
// forEach로 누적
let itemsHtml = '';
phones.forEach((it) => {
itemsHtml += cardItem(it);
});
// 행 래퍼로 감싸서 반환
return `
<div class="row g-3">
${itemsHtml}
</div>
`;
}
3. DB 생성
use lg_uplus_clone;
drop table if exists phone;
CREATE TABLE `phone` (
`no` int NOT NULL AUTO_INCREMENT,
`title` varchar(45) NOT NULL,
`price` int NOT NULL,
`img` varchar(200) DEFAULT NULL,
PRIMARY KEY (`no`)
) ;
INSERT INTO `phone` (`title`,`price`,`img`) VALUES ('iPhone 17 Pro',183000,'https://www.lguplus.com/static/pc-contents/images/prdv/20250912-090008-227-4euoWeLA.png');
INSERT INTO `phone` (`title`,`price`,`img`) VALUES ('iPhone 17 Pro Max',206000,'https://www.lguplus.com/static/pc-contents/images/prdv/20250910-083946-071-9WBJDiuU.png');
INSERT INTO `phone` (`title`,`price`,`img`) VALUES ('iPhone Air',174000,'https://www.lguplus.com/static/pc-contents/images/prdv/20250911-011759-282-0K8FJVQd.png');
INSERT INTO `phone` (`title`,`price`,`img`) VALUES ('갤럭시 S25 Edge',169000,'https://www.lguplus.com/static/pc-contents/images/prdv/20250509-054654-432-IsbXiKp0.png');
INSERT INTO `phone` (`title`,`price`,`img`) VALUES ('갤럭시 S25 FE',146000,'https://www.lguplus.com/static/pc-contents/images/prdv/20250918-042342-680-1xc4hIoL.png');
INSERT INTO `phone` (`title`,`price`,`img`) VALUES ('갤럭시 Z Fold7',210000,'https://www.lguplus.com/static/pc-contents/images/prdv/20250910-024220-952-GEwhPicD.png');
INSERT INTO `phone` (`title`,`price`,`img`) VALUES ('iPhone 16',164000,'https://www.lguplus.com/static/pc-contents/images/prdv/20250910-052739-762-pGCwOfjp.png');
INSERT INTO `phone` (`title`,`price`,`img`) VALUES ('iPhone 16 Plus',171000,'https://www.lguplus.com/static/pc-contents/images/prdv/20240923-011248-690-lQ4TSHC5.png');
INSERT INTO `phone` (`title`,`price`,`img`) VALUES ('iPhone 16e',147000,'https://www.lguplus.com/static/pc-contents/images/prdv/20250220-034645-151-AjCrRPeb.png');
INSERT INTO `phone` (`title`,`price`,`img`) VALUES ('갤럭시 S25',155000,'https://www.lguplus.com/static/pc-contents/images/prdv/20250120-015840-177-JN3pbd7R.png');
INSERT INTO `phone` (`title`,`price`,`img`) VALUES ('갤럭시 S25+',164000,'https://www.lguplus.com/static/pc-contents/images/prdv/20250120-030631-886-0Qduq7AU.png');
INSERT INTO `phone` (`title`,`price`,`img`) VALUES ('갤럭시 S25 Ultra',180000,'https://www.lguplus.com/static/pc-contents/images/prdv/20250120-031949-593-U2AL6d3O.png');
INSERT INTO `phone` (`title`,`price`,`img`) VALUES ('샤오미 Redmi Note 14 5G',69000,'https://www.lguplus.com/static/pc-contents/images/prdv/20250624-031334-789-KUDzELDt.png');
INSERT INTO `phone` (`title`,`price`,`img`) VALUES ('Galaxy Buddy3',71000,'https://www.lguplus.com/static/pc-contents/images/prdv/20240419-090638-685-0RpiBGG7.jpg');
INSERT INTO `phone` (`title`,`price`,`img`) VALUES ('갤럭시 S24 FE',146000,'https://www.lguplus.com/static/pc-contents/images/prdv/20241031-032236-677-ZaRQ7pcK.png');
4. 백엔드 구현
- Phone.java 생성
package com.ureca.web.model.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Phone {
private int no,price;
private String title, img;
}
- PhoneDao.java 생성
package com.ureca.web.model.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.ureca.web.model.dto.Phone;
@Mapper
public interface PhoneDao {
public List<Phone> getPhones();
}
- phone.xml 생성
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ureca.web.model.dao.PhoneDao">
<select id="getPhones" resultType="Phone">
SELECT * FROM phone limit 6
</select>
</mapper>
- PhoneService.java 생성
package com.ureca.web.model.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ureca.web.model.dao.PhoneDao;
import com.ureca.web.model.dto.Phone;
@Service
public class PhoneService {
@Autowired
PhoneDao phoneDao;
public List<Phone> getPhones(){
return phoneDao.getPhones();
}
}
- PhoneController.java 생성
package com.ureca.web.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ureca.web.model.dto.Phone;
import com.ureca.web.model.service.PhoneService;
@RestController
public class PhoneController {
@Autowired
PhoneService phoneService;
@GetMapping("/getPhones")
public List<Phone> getPhones(){
return phoneService.getPhones();
}
}
PhoneController의 경우 Get 방식으로 통신을 한다.
5. phone.js에 fetch 요청 구현
async 함수로 바꾸고 아래의 코드를 추가한다.
const response = await fetch('http://localhost:8080/getPhones');
const phones = await response.json();
phone.js에서 async로 리턴하면 Promise 객체로 리턴된다. 따라서 router.js에서도 Promise를 처리해야 한다.
router() 함수를 async로 변경한다.
실행 결과

느낀 점
기능을 하나씩 추가하는 재미가 있었다. 어떤 흐름으로 이루어지는지 이해가 됐다.
하지만 기능이 많아지면 얽히고설킬 텐데 그 때가 걱정되기도 한다. 배운대로 해보면서 하나씩 늘려가야될 것 같다.
'TIL' 카테고리의 다른 글
| JWT와 Session의 개념 및 차이 (0) | 2025.10.20 |
|---|---|
| 20251013 회원가입 암호화 (0) | 2025.10.13 |
| 20250930 uplus clone site 다시 만들기 (0) | 2025.09.30 |
| 20250925 DB (1) | 2025.09.25 |
| 20250924 위상정렬, 투포인터, 슬라이딩 윈도우 알고리즘 (0) | 2025.09.24 |