쇼핑몰 웹사이트 만들어보기 - 카테고리를 통한 검색

Shiba·2024년 3월 30일
0

프로젝트 및 일기

목록 보기
4/29
post-thumbnail

일단 초기화면을 완성했으니 이제 회원 시스템을 스프링을 통해 구현해보자.

앞에서 회원 시스템은 다음과 같이 기획했다

회원이 존재하며, 구매한 금액에 따라 혜택을 받도록 하자. 혜택에는 특정 금액 이상 누적 구매 달성 시, 등급이 오르며 그에 따른 쿠폰을 주는 방식이 가장 보편적으로 보인다

구현해야할 것
1.회원정보
2.회원가입, 로그인, 로그아웃
3.회원정보 변경 창
4.등급 시스템

다음 시스템을 구현해보도록하자. 일단 계층구조는 토비의 스프링과 김영한님의 스프링 강의와 같이
컨트롤러, 서비스, 리포지토리, 도메인을 만들 것이고, 인터페이스를 통해 의존하도록하여 관심을 분리시켜보자.

먼저 회원 정보 도메인을 만들어보자.
회원정보에는 어떤 것들이 필요할까? 아이디,비밀번호,이름은 기본이며 쇼핑몰이기 때문에 주소도 필요할 것이다. 추후에 넣을 수도 있는 생일 쿠폰 기능을 위해 생일도 넣어두도록 하자.

package com.shoppingmall.domain;

public class User {
    private String id;
    private String pw;

    private String name;
    private String place;
    private Day birthday;

    public void setId(String id) { this.id = id;}
    public String getId() {
        return id;
    }

    public String getPw() {
        return pw;
    }

    public void setPw(String pw) {
        this.pw = pw;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPlace() {
        return place;
    }

    public void setPlace(String place) {
        this.place = place;
    }

    public Day getBirthday() {
        return birthday;
    }

    public void setBirthday(Day birthday) {
        this.birthday = birthday;
    }

    User() {}
}
package com.shoppingmall.domain;

public class Day {
    int year;
    int month;
    int day;

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public int getMonth() {
        return month;
    }

    public void setMonth(int month) {
        this.month = month;
    }

    public int getDay() {
        return day;
    }

    public void setDay(int day) {
        this.day = day;
    }

    public Day(String[] d){
        for(int i = 0; i<3; i++){
            this.year = year;
            this.month = month;
            this.day = day;
        }
    }
}

다음과 같이 순수하게 getter,setter만 존재하며, 계층간 데이터를 교환하기위해 만든 데이터 객체를 DTO(Data Transfer Object)라고 한다고 한다. 그리고 이 데이터 객체를 사용하기위한 함수를 짜놓은 객체를 토비에서 그렇게 만들던 DAO(Data Access Object)라고한다.

이제 여기에 Enum객체를 활용해서 등급을 만들어서 넣어보자.

package com.shoppingmall.domain;

public enum Level {
    BASIC(1), NORMAL(2), VIP(3), VVIP(4); // enum 오브젝트 정의

    private final int value;

    Level(int value) { // 생성자 만들기
        this.value = value;
    }

    public int intValue() { // 값을 가져오는 메소드
        return value;
    }
}

리포지토리, 서비스는 영한님의 스프링강의를 참고하여 작성했다. (강의나 책을보며 따라치면서 배우다가 직접 하려고하니 많이 헷갈린다) 의존관계를 설정하기위해 Config클래스를 만들어 리포지토리와 서비스를 빈으로 추가해주었다.

토비님과 영한님 둘다 테스트를 아주중요하게 생각하셨다. 그래서 테스트를 짜서 모두 맞는지, 예외는 없는지 확인해보자.

테스트할 메소드는 다음과 같다

리포지토리 테스트

  • 회원 추가 메소드
  • 레벨 리턴 메소드
  • 아이디가 존재하는지 찾아내는 메소드

서비스 테스트

  • 회원 등록 메소드
  • 중복 아이디 거르는 메소드
package com.shoppingmall.repository;

import com.shoppingmall.domain.Level;
import com.shoppingmall.domain.User;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;

public class MemoryUserRepostoryTest {
    MemoryUserRepository repository;

    @AfterEach
    public void afterEach(){
        repository.clearStore();
    }

    @Test
    public void add() { //사용자가 잘 들어갔는지 확인용
        User user = new User();
        user.setId("test1");

        repository.add(user);

        User res = repository.findById(user.getId()).get();
        assertThat(res).isEqualTo(user);
    }

    @Test
    public void valueOf(){
        assertThat(repository.valueOf(1)).isEqualTo(Level.BASIC);
        assertThat(repository.valueOf(2)).isEqualTo(Level.NORMAL);
        assertThat(repository.valueOf(3)).isEqualTo(Level.VIP);
        assertThat(repository.valueOf(4)).isEqualTo(Level.VVIP);
        Assertions.assertThrows(AssertionError.class, () ->{
            repository.valueOf(5);
        });
    }

    @Test
    public void findById() {
        User user1 = new User();
        user1.setId("test1");
        repository.add(user1);

        User user2 = new User();
        user2.setId("test2");
        repository.add(user2);

        User res = repository.findById("test1").get();
        assertThat(res).isEqualTo(user1);
    }
}
package com.shoppingmall.service;

import com.shoppingmall.domain.User;
import com.shoppingmall.repository.MemoryUserRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

public class UserServiceTest {

    UserService userService;
    MemoryUserRepository userRepository;

    @BeforeEach
    public void beforeEach() {
        userRepository = new MemoryUserRepository();
        userService = new UserService(userRepository);
    }

    @AfterEach
    public void afterEach(){
        userRepository.clearStore();
    }

    @Test
    public void join(){
        User user = new User();
        user.setId("test1");

        String saveId = userService.join(user);

        User isUser = userRepository.findById(saveId).get();
        assertEquals(user.getId(), isUser.getId());
    }

    @Test
    public void validateDupUser(){
        User user1 = new User();
        user1.setId("test1");

        User user2 = new User();
        user2.setId("test1");

        userService.join(user1);

        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> userService.join(user2));

        assertThat(e.getMessage()).isEqualTo("이미 존재하는 아이디입니다.");
    }
}


테스트코드가 약간 엉성한 것같은 느낌이 약간 든다.(특히 레벨 테스트 부분이 직접 숫자를 넣음으로서 테스트 하는 방식이라 다른 값이 들어왔을 때를 테스트하려면 숫자를 계속 바꿔야하는 불편함이 있는 것 같다.) 추후에 새로운 방식이 생각나면 바꿔보도록하자

이제 회원 가입 페이지를 만들어서 직접 테스트를 해보도록 하자.

간단하게 템플릿만 만들었다.

<!DOCTYPE HTML>
<meta charset="UTF-8">
<body>
<div class="container">
  <form action="/user/new" method="post">
    <div class="form-group">
      <label for="id">아이디</label>
      <input type="text" id="id" name="id" placeholder="아이디를
입력하세요">
      <label for="pw">비밀번호</label>
      <input type="text" id="pw" name="pw" placeholder="비밀번호를
입력하세요">
      <label for="name">이름</label>
      <input type="text" id="name" name="name" placeholder="이름을
입력하세요">
      <label for="place">주소</label>
      <input type="text" id="place" name="place" placeholder="주소를
입력하세요">
      <label for="birth">생일</label>
      <input type="text" id="birth" name="birth" placeholder="생일을
입력하세요">
    </div>
    <button type="submit">등록</button>
  </form>
</div> <!-- /container -->
</body>
</html>

이제 이 웹사이트를 컨트롤러를 통해 매핑해서 사용해보자

package com.shoppingmall.controller;

import com.shoppingmall.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class UserController {
    private final UserService userService;

    public UserController(UserService userService){
        this.userService=userService;
    }
	
    //로그인 창 매핑
    @GetMapping(value = "/user/login")
    public String login(){
        return "user/login";
    }

	//회원가입 창 매핑
    @GetMapping(value = "/user/new")
    public String createForm(){
        return "user/createUserForm";
    }
}

이제 서버를 열어 확인해보려던 찰나.. 서버를 열어서 확인해보니 css와 js파일이 들고와지지않아 화면이 깨진채로 보였다. 서버를 열지않고 실험을 했을때와 경로가 달라서 그런 것이다. 본래는 "/static/script.js"였다면 이를 js폴더를 만들어 js파일을 저장해두도록 하고, 경로를 "/js/script.js"로 바꾸니 확실하게 연동이 되었다.

개발자도구를 사용하여 css도 실시간으로 색을 바꿔가며 홈페이지의 색감을 살짝 변경해보았다.(개발자 도구가 진짜 유용한 도구인 것 같다!)

또한 해당 부분을 수정하면서 js코드에서 연동이 안되는 코드를 몇개 수정하였다.

먼저 로그인 창을 본래는 html을 직접 연결하여 열었다면 이제는 컨트롤러를 통해 매핑해서 열도록 바꾸었다.

function redirectToLoginPage() {
        window.location.href = "/user/login";
    }

또한 카테고리를 누르면 카테고리를 검색어로 사용하여 검색결과 창을 가려고 하였는데, 세부 카테고리 부분을 누르면 항상 큰 카테고리 부분이 쿼리로 들어오는 버그가 존재했다. 해당 부분에 대해 구글링과 지피티를 통해 알아보니 이벤트 버블링(event bubbling)이라는 현상이 발생한 것이라고 한다.

이벤트 버블링이란 하위 이벤트가 상위 이벤트로 전달되는 것이라고 한다. 즉 세부 카테고리를 눌러도 그의 상위 이벤트인 큰 카테고리를 누르는 이벤트로 전달이 되어 항상 큰 카테고리가 쿼리값으로 받아지던 것이다. 해당 버그를 고치기 위해 지피티는 해당 방법을 추천했다.

detailItem.addEventListener("click", () => {
	// 사용자가 카테고리를 클릭했을 때 실행될 함수 호출
    redirectSearchResult(detail);
    event.stopPropagation(); //이벤트가 전파되는 것을 막음
});

stopPropagation()을 사용하면 상위 이벤트로 전파되지않고 모든 이벤트를 종료시킨다고 한다. 그래서 만약 상위 태그에서 사용하고 싶은 이벤트가 있다면 사용할 수 없는 메소드인 것 같다. 그리고 event를 통한 이벤트의 제어가 코드를 꼬이게할 수 있다는 우려때문인지 event가 event로 되어있다. 다르게 처리할 수 있는 방법이 있는지 공부해보아야겠다.

마지막으로, 쿼리값을 받고, 이를 html에 띄우기 위해 Thymeleaf를 사용했다.
Thymeleaf도 영한님 강의에서 잠깐보고 지나간 것이라 따로 공부해서 정리해보아야겠다.

컨트롤러를 통해 쿼리를 검색결과창과 매핑하는 코드

package com.shoppingmall.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

@Controller
public class SearchController {

    @GetMapping("/search")
    public String search(@RequestParam("query") String query, Model model) {
        // 여기서 query는 요청 파라미터의 이름입니다.
        // 예를 들어, /search?query=검색어 형식으로 요청이 들어온다면,
        // "검색어" 부분이 query 매개변수로 전달됩니다.

        // 이후 검색어에 따라 데이터를 조회하거나 다른 비즈니스 로직을 수행할 수 있습니다.
        // 이 예제에서는 간단히 검색어를 모델에 추가하여 검색결과 페이지로 전달합니다.
        model.addAttribute("query", query);

        // 검색결과를 표시할 템플릿 이름을 반환합니다.
        return "searchResult"; // search-result.html과 같은 템플릿 파일을 찾게 됩니다.
    }
}
<!--searchResult.html-->

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>검색결과</title>
</head>
<body>
<form action="/search">
  	<!--타임리프를 통해 query값을 매핑해서 화면에 보여주기-->
    <h1><span th:text="${query}"></span>검색결과 페이지 입니다</h1>
</form>
</body>
</html>
//검색결과 창으로 이동하는 메소드
 function redirectSearchResult(search) {
        window.location.href = "/search?query=" + encodeURIComponent(search);
    }

다음은 결과물이다

검색결과 매핑

회원가입 및 로그인 창 매핑

다음시간에는 로그인창, 회원가입창을 css를 통해 꾸며준 후, 데이터베이스를 선택해서 데이터베이스를 연결해보는 시간을 가져보도록하자 연결을 하였다면 로그인,로그아웃, 회원정보 변경, 등급시스템도 제대로 구현해보자.

이번 시간에는 처음으로 스프링을 내가 만드려고 하는 것을 위해 사용해보았다. 공부를 할때는 다 안다고 생각했는데 막상 직접 설계하고 코드를 짜려고하니 실수가 많이나왔고, 버그도 간단한 버그지만 많이 만나게되었다. 심지어는 사용하려는 애노테이션간의 충돌이 일어나는 것도 경험해보게 되었다.(@Configuration과 @Repository간의 충돌)
결국 내 것으로 만들기 위해서는 내가 만들고 싶은 것을 자기가 직접 머리와 손을 써서 무언가를 만들어내는 경험이 필요하다는 것을 느끼게 되었다.

profile
모르는 것 정리하기

0개의 댓글