로그인을 해야지 저장할 수 있도록 구현했다.
검색 바에서 책을 검색하고 읽고 싶어요를 누르면
개발자 도구에 저장 성공: 책이 성공적으로 저장되었습니다.라고 출력이 된다.
DB에 역시 저장이 된다. 팀원에게 알려줬더니 한가지 문제점을 말해줬다.
email book_id가 있으면
1@1.com 1
1@1.com 2
2@1.com 3
이런식으로 된다는 것을 발견해준 것이다.
각 이메일 별 book_id가 다르도록 저장을 되도록 수정해야된다.
음하하 이제 이렇게 book_id는 순차적으로 이메일이 달라도 같은 책 저장되게 그리고 책을 중복으로 담지 않게 저장했다. <- 거의 하루종일 잡고 있는듯?
이제 프론트 화면에서 로그인을 하면 저장된 책은 표시되게 하고 싶다. 클론한 사이트도 보니까 그렇게 구현했다.
이 순서를 항상 기억하자! 물론 파일명은 다르지만 동작 순서는 같다!
이미 담겨있습니다. 구현 잘되고..
아니..분명히..로그아웃되어 있을때 로그아웃되었다는 토스트 문구가 떴는데.. 왜
갑자기...
그리고 로그인하면 책을 담으면
서버 응답 없음이 나온다.. 불과... 5분전만해도 작동이 잘되었는데.. 이거 해결해보고 자보겠다.. T0T
전원 껐다 켜보고 다시 해봐야겠다. 라고 창을 하나씩 끄던 찰나!
배경화면에 창 하나가 있는 북트래커 웹 발견😲
이미 sql 서버에는 로그인 정보를 삭제해놔서 로그아웃 상태이다.
오.. 한번 눌러보니 먼저 로그인해주세요! 토스트 문구 나오고
자! 이번에는 로그인을 해보겠다.
오! 책이 담긴다!
중복 저장도 안되고!
그러면 창이 중복으로 켜있을때라고 해야되나? 그럴때는 저장이 오류가 나는건가?
🤖 " 서버에서 409 Conflic반환이 되어 오류가 난다고 한다. 그 외 다른 이유들이 있다고 한다. 세션 스토리지의 토큰 창이 독립적이므로 서버에서 400 bad request를 반환한다고 한다 또는 비동기 요청 타이밍 문제 -> 여러 창에서 거의 동시에 요청이 발생해서 book_id 계산이 꼬일 수 있다." 라고 답해줬다.
const token = sessionStorage.getItem('Authorization');
이미 토큰은 로컬에서 가져와서 세션 스토리지 오류는 아닌듯하다.
아이콘을 fontawsome 아이콘으로 바꾸고 싶었는데.. 템플릿 리터럴을 써도 오류가 난다.
function showToast(message) {
const toast = document.createElement('div');
toast.classList.add('toast-message');
toast.innerText = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('fade-out');
setTimeout(() => toast.remove(), 500);
}, 3000);
}
toast.innerText가 아닌 toast.innerHTML로 바꿔야한다.
function showToast(message) {
const toast = document.createElement('div');
toast.classList.add('toast-message');
toast.innerHTML = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('fade-out');
setTimeout(() => toast.remove(), 500);
}, 3000);
}
innerText vs innerHTML 차이점
innerText: HTML 태그를 문자열 그대로 출력한다. 태그가 실제 HTML로 해석되지 않는다.
innerHTML: HTML 태그를 실제 요소로 해석해 브라우저에 표시한다.
search.js 흐름 정리
api/book/search → 도서 검색 api/book/save → 책 저장 api/book/save → 사용자 저장된 책 목록 조회
1. 도서 검색 /api/books/search - 프론트엔드
- searchBooks(keyword) 함수에서 사용자가 검색어 요청하면 API 요청
- axios.get('/api/books/search?keyword='검색어') 실행
axios
.get(
`http://localhost:8080/api/books/search?keyword=${encodeURIComponent(
keyword
)}`
)
- 응답 받은 데이터를 renderSearchResults()로 전달해 화면에 표시
.then((response) => {
console.log(response.data); // 응답 확인
renderSearchResults(response.data);
})
프론트엔드 화면에 출력되는 태그들이다. 이미지, 책 제목, 작가, 출판사, 읽고 싶어요 아이콘
if (data && data.item && data.item.length > 0) {
data.item.forEach((book) => {
const div = document.createElement('div');
div.classList.add('book-item');
div.innerHTML = `
<img class="book-cover" src="${book.cover}" alt="${book.title}">
<div class="book-info">
<h4 class="book-title">${book.title}</h4>
<p class="book-author">${book.author} · ${book.publisher}</p>
<button class="status-btn"><i class="fa-solid fa-plus"></i>읽고싶어요</button>
</div>
`;
1. 도서 검색 /api/books/search - 백엔드
// 알라딘 API를 활용한 도서 검색 기능
@GetMapping("/search")
public ResponseEntity<String> searchBooks(@RequestParam String keyword) {
// 알라딘 API 호출 URL
String apiUrl = "https://www.aladin.co.kr/ttb/api/ItemSearch.aspx" +
"?ttbkey=" + aladinApiKey +
"&Query=" + keyword +
"&QueryType=Keyword" +
"&MaxResults=50" +
"&start=1&SearchTarget=Book&output=js&Version=20131101";
// 외부 API 호출
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(apiUrl, String.class);
// 프론트엔드로 결과 전달
return ResponseEntity.ok(response);
}
- 클라이언트에서 /api/books/search 엔드포인트로 GET요청
엔드포인트란?
쉽게말해서 URL을 의미
사용자의 정보를 가져오는 API가 있다라고 가정을 하면?
엔드포인트: https://api.example.com/users/1
HTTP 메서드: GET
설명: ID가 1인 사용자 정보를 조회
https://jsonplaceholder.typicode.com/users/1
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz"
} // 엔드포인트에 get요청을 보내면 ,id가 1인 사용자 정보를 받을 수 있다.
✅ 주소역할
✅ 클라이언트-서버 통신
✅ RESTful API에서 중요! 왜? 리소스를 나타내는 방식
RESTful api란 뭘까?
웹에서 클라이언트와 서버가 데이터를 주고 받을 때 규칙을 정한 것
엔트포인트(url)로 표현하고 http 메서드(get, post 등)을 활용해 상태를 주고 받는 방식
GET방식과 POST 방식의 차이는?
GET방식
데이터를 조회할 때 사용
POST 방식
데이터를 생성하거나 수정할 때 사용
- 위의 설명에 나와있듯 결과를 프론트엔드에 JSON 형태로 반환된다.
2. 도서 검색 /api/books/save - 프론트엔드
- 사용자가 읽고 싶어요 버튼 클릭시 saveBookStatus(book) 실행
- axios.post('/api/books/save', bookData, { headers: { Authorization: token } }) 호출
function saveBookStatus(book) {
const token = sessionStorage.getItem('Authorization'); // 로그인 토큰 가져오기
axios
.post(
'http://localhost:8080/api/books/save',
{
title: book.title,
author: book.author,
publisher: book.publisher,
cover: book.cover,
status: '읽고 싶어요',
},
{
headers: { Authorization: token },
}
)
- 응답 메시지에 따라 각각의 토스트 메시지가 표시
// 검색한 책을 DB에 저장하는 기능 (읽고 싶어요)
@PostMapping("/save")
public ResponseEntity<String> saveBook(@RequestHeader String authorization, @RequestBody Book book) {
try {
// 로그인 토큰 검증
Login loginInfo = userService.checkToken(authorization);
if (loginInfo == null) {
return ResponseEntity.status(401).body("Unauthorized: 유효하지 않은 토큰");
}
// 현재 로그인한 사용자의 이메일을 설정
book.setEmail(loginInfo.getEmail());
// 중복 검사 (같은 사용자 + 같은 제목)
Book existingBook = bookService.getBookByEmailAndTitle(book.getEmail(), book.getTitle());
if (existingBook != null) {
return ResponseEntity.status(409).body("이미 담겨 있습니다."); // 409 응답 반환
}
// 중복이 아니면 책 추가
bookService.insertBook(book);
return ResponseEntity.ok("책이 성공적으로 저장되었습니다.");
} catch (Exception e) {
return ResponseEntity.status(500).body("책 저장 중 오류 발생: " + e.getMessage());
}
}
- 사용자가 로그인 상태 여부 authorization 헤더 토큰 검증
- 401 → 유효하지 않은 토큰
- 409 → 이미 담겨져 있음
- 500 → 오류 발생
- 책 담기 성공
사용자가 추가할 book_id 값을 가져오는 역할
<select id="getNextBookIdByEmail" parameterType="String" resultType="Integer">
select coalesce(max(book_id) + 1, 1) from book where email = #{email}
</select>
이메일을 가진 사용자 중 큰 값을 찾아서 book_id 값을 계산할 때 +1을 더한다. 만일 책이 한권도 없다면 1을 기본 값으로 설정한것이다.
coalesce는 null 값이 나올 경우 대체 값을 지정하는 함수
'💡 URECA > 📽️ 프로젝트' 카테고리의 다른 글
[URECA] 독서 목표 설정 및 챌린지 달성 지원 웹사이트 구현을 통한 웹 아키텍처 이해 #7 (1) | 2025.03.20 |
---|---|
[URECA] 독서 목표 설정 및 챌린지 달성 지원 웹사이트 구현을 통한 웹 아키텍처 이해 #6 (0) | 2025.03.18 |
[URECA] 독서 목표 설정 및 챌린지 달성 지원 웹사이트 구현을 통한 웹 아키텍처 이해 #4 (0) | 2025.03.17 |
[URECA] 독서 목표 설정 및 챌린지 달성 지원 웹사이트 구현을 통한 웹 아키텍처 이해 #3 (2) | 2025.03.16 |
[URECA] 독서 목표 설정 및 챌린지 달성 지원 웹사이트 구현을 통한 웹 아키텍처 이해 #2 (0) | 2025.03.15 |