[Baeg-won Clothing Gallery] 9. PROFILE 페이지 구현

2023. 7. 31. 15:41·🚗 Backend Toy Project/Baeg-won Clothing Gallery

📝 Outline

  • 회원정보 수정을 위한 Profile 페이지 구현을 완료했다.
  • 아마 지금까지 구현한 페이지들 중에서 가장 많은 로직과 코드가 들어간 페이지가 아닐까 싶다.
  • 구현 디자인은 다음과 같다.

  • 해당 페이지에서는 Username을 제외한 계정 정보를 수정할 수 있으며, 배송지를 추가할 수도 있다.
  • Account와 Address 입력 데이터 모두 유효성 검사가 포함되어 있으며, 당연하게도 유효성 검사를 통과하지 못할 경우 데이터가 DB에 반영되지 않는다.

📝 Review

  • 대부분의 로직이 클라이언트 쪽에 집중되어 있고, 주로 사용자의 편의를 위해 작성된 로직이 대부분이라 코드의 양은 많았지만 사실상 핵심적인 부분은 크게 두 가지 정도로 나눌 수 있을 것 같다.
  • 하나는 유효성 검사, 다른 하나는 카카오 주소 API이다.

 

💬 Validation

  • 회원정보 수정을 주 목적으로 하는 해당 페이지에서는 유효성 검사가 필연적으로 들어가야 했다.
  • 유효성 검사는 크게 클라이언트측과 서버측으로 나뉘며, 각기 다른 방법으로 유효성 검사를 수행한다.
  • 먼저 클라이언트측 유효성 검사 방식부터 알아보자.
  <!-- Input Account Info -->
  <v-form
    ref="accountForm"
    v-model="accountValid"
    lazy-validation
  >
    <div class="div-grid_container">
      <div>
        <div>Username</div>
        <v-text-field
          v-model="account.username"
          class="v-text-field-input_account_info"
          dense
          color="#666666"
          disabled
        ></v-text-field>
      </div>
      <div>
        <div>Password</div>
        <v-text-field
          v-model="account.password"
          class="v-text-field-input_account_info"
          :append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
          :type="showPassword ? 'text' : 'password'"
          clearable
          dense
          counter
          color="#666666"
          :rules="accountRules.password"
          @click:append="showPassword = !showPassword"
        ></v-text-field>
      </div>
      <div>
        <div>Nickname</div>
        <v-text-field
          v-model="account.nickname"
          class="v-text-field-input_account_info"
          clearable
          dense
          counter
          color="#666666"
          :rules="accountRules.nickname"
        ></v-text-field>
      </div>
      <div>
        <div>Phone</div>
        <v-text-field
          v-model="account.phone"
          class="v-text-field-input_account_info"
          dense
          color="#666666"
          @keyup="getPhoneMask(account.phone)"
          :rules="accountRules.phone"
        ></v-text-field>
      </div>
    </div>
  </v-form>
  <!-- Input Account Info End -->
  • vuetify에서 제공하는 <v-form>은 기본적으로 validate() 기능을 제공한다.
  • 사용 방법 또한 매우 간단한데, 우선 위와 같이 <v-form> 태그에 ref 속성을 지정해주고 해당 폼 태그 안에 포함되어 있는 요소에 rules 속성을 통해 유효성 검사 조건을 설정해준다.
  • rules 속성에 적용되는 검사 조건은 다음과 같은 방식으로 정의해준다.
export default {
  data() {
    return {
      ...

      accountRules: {
        password: [
          v => !!v || "Password is required", 
          v => (v && 8 <= v.length && v.length <= 16 && /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.`!@#$%^&*?])[A-Za-z\d@.`!@#$%^&*?]{8,16}$/.test(v)) || "Password must be between 8 and 16 characters including english, numbers, and special characters."
        ],
        nickname: [
          v => !!v || "Nickname is required",
          v => (v && 2 <= v.length && v.length <= 10) || "Nickname must be between 2 and 10 characters"
        ],
        phone: [
          v => !!v || "Phone is required",
          v => /\d{3}-\d{3,4}-\d{4}/.test(v) || "Phone must be valid"
        ]
      },

      ...
    };
  },
}
  • 이렇게 간단히 유효성 검사를 수행할 수 있으며, 그 결과에 따라 DB에 반영할지 말지를 결정하기 위해 다음과 같은 코드를 작성해주었다.
updateInfo() {
  if(this.$refs.accountForm.validate()) {
    this.$axios.put("/customer/update", this.account).then((res) => {
      this.account = res.data;
      this.beforeAccount = JSON.parse(JSON.stringify(res.data));  // Deep copy

      this.successAlert = true;

      setTimeout(() => {
        this.successAlert = false;
      }, 2000);
    });
  } else {
    this.errorAlert = true;

    setTimeout(() => {
      this.errorAlert = false;
    }, 2000);
  }
},
  • this.$refs.accountForm.validate()를 통해 유효성 검사의 결과를 확인할 수 있으며, 그 결과에 따라 분기하고 있다.
  • 결과를 살펴보면 다음과 같다.

  • 서버측에서는 spring-boot-starter-validation을 이용하여 유효성 검사를 실시하였다.
  • 이를 위해 먼저 아래와 같이 의존성 추가를 수행준다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
  • 이후 DTO 클래스에 다음과 같이 어노테이션을 설정해주었다.
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class UpdateInfoDto {

    private Long id;

    @NotBlank(message = "Username is required")
    @Pattern(regexp = "^[a-zA-Z0-9+-\\_.]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$", message = "E-mail must be valid")
    private String username;

    @NotBlank(message = "Password is required")
    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[.`!@#$%^&*?])[A-Za-z\\d@.`!@#$%^&*?]{8,16}$",
            message = "Password must be between 8 and 16 characters including english, numbers, and special characters.")
    private String password;

    @NotBlank(message = "Nickname is required")
    @Size(min = 2, max = 10, message = "Nickname must be between 2 and 10 characters.")
    private String nickname;

    @NotBlank(message = "Phone is required")
    @Pattern(regexp = "\\d{3}-\\d{3,4}-\\d{4}", message = "Phone must be valid")
    private String phone;
}
  • 이후 Controller에서 이를 캐치하기 위해 @Valid 어노테이션과 BindingResult를 사용한다.
@RequestMapping("customer")
@RestController
public class CustomerApiController {

    @Autowired
    private CustomerService customerService;

    ...

    @PutMapping("/update")
    public ResponseEntity<?> updateInfo(@Valid @RequestBody UpdateInfoDto updateInfoDto, BindingResult bindingResult) {
        if(bindingResult.hasErrors()) {
            Map<String, String> validatorResult = customerService.validateHandling(bindingResult);

            return new ResponseEntity<>(validatorResult, HttpStatus.BAD_REQUEST);
        }

        return new ResponseEntity<>(customerService.updateInfo(updateInfoDto), HttpStatus.OK);
    }
}
  • 만약 유효성 검사에 통과하지 못했다면 bindingResult.hasErrors()를 통해 customerService에 정의되어 있는 validateHandling 메서드가 호출될 것이며, 해당 메서드는 다음과 같이 정의되어 있다.
@Service
public class CustomerService {

    ...

    @Transactional(readOnly = true)
    public Map<String, String> validateHandling(BindingResult bindingResult) {
        Map<String, String> validatorResult = new HashMap<>();

        for(FieldError error : bindingResult.getFieldErrors()) {
            String validKeyName = String.format("valid_%s", error.getField());
            validatorResult.put(validKeyName, error.getDefaultMessage());
        }

        return validatorResult;
    }
}
  • 즉, 유효성 검사에 실패한 요소를 추출하여 실패 원인 메시지와 함께 Map 자료구조로 저장하는 것이다.
  • 이후 Postman을 사용하여 유효성 검사에 실패하는 요청을 수행해보면 다음과 같은 결과를 확인할 수 있다.

 

💬 카카오 주소 API

  • 주소 데이터를 추가하는 작업을 수행하면서 카카오 주소 API를 사용하게 되었다.
  • 그 사용 방법은 매우 간단했다.
  • 우선 아래 코드를 public 폴더에 있는 index.html 파일의 <head> 태그 안에 추가해준다.
<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
  • 이후 API를 사용할 vue 파일에 다음과 같이 메서드를 정의해준다.
// Kakao Address API
openPostCode() {
  new window.daum.Postcode({
    oncomplete: (data) => {
      this.zipcode = data.zonecode;
      this.streetAddress = data.roadAddress;
    }
  }).open();
},
  • 마지막으로 해당 메서드가 적절히 호출될 수 있도록 이벤트를 설정해주었다.
  <!-- Set Address Dialog -->
  <v-form
    ref="addressForm"
    v-model="addressValid"
  >
    <div class="mt-3">
      <v-dialog v-model="addressDialog" max-width="600px">
        <template v-slot:activator="{ on, attrs }">
          <v-btn class="btn-account" dark v-bind="attrs" v-on="on" elevation="0">
            Add New Address
          </v-btn>
        </template>
        <v-card>
          <v-card-title>
            <span class="text-h5">Add New Address</span>
          </v-card-title>
          <v-card-text>
            <v-container>
              <v-row>
                <v-col cols="12" sm="6">
                  <v-text-field
                    v-model="addressName"
                    label="Name*"
                    :rules="addressRules.addressNameRules"
                    required
                    hint="Please set the name of your address."
                  ></v-text-field>
                </v-col>
                <v-col cols="12" sm="6">
                  <v-text-field
                    v-model="zipcode"
                    label="Zip / Postal code*" 
                    :rules="addressRules.addressCodeRules"
                    required
                    readonly
                    @focus="openPostCode"
                  ></v-text-field>
                </v-col>
                <v-col cols="12">
                  <v-text-field
                    v-model="streetAddress"
                    label="Street address*"
                    :rules="addressRules.streetAddressRules"
                    required
                    readonly
                    hint="Please enter it through the Zip / Postal code."
                  ></v-text-field>
                </v-col>
                <v-col cols="12">
                  <v-text-field
                    v-model="detailAddress"
                    label="Detail address"
                  ></v-text-field>
                </v-col>
                <v-col cols="12" class="mt-n7 ml-n1">
                  <v-checkbox
                    v-model="isDefault"
                    label="Make this my default address"
                  ></v-checkbox>
                </v-col>
              </v-row>
            </v-container>
            <small>*indicates required field</small>
          </v-card-text>
          <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn color="darken-1" text @click="resetDialog">
              Close
            </v-btn>
            <v-btn color="darken-1" text @click="addNewAddress" :disabled="!addressValid">
              Save
            </v-btn>
          </v-card-actions>
        </v-card>
      </v-dialog>
    </div>
  </v-form>
  <!-- Set Address Dialog End -->
  • 자세히 살펴보면, zipcode를 입력하는 요소에서 @focus를 통해 위에서 정의한 메서드를 호출하는 것을 확인할 수 있다.
  • 이렇게 하면, 위 메서드가 호출될 경우 주소 검색을 위한 팝업창이 뜨게 되고, 해당 팝업창 내에서 주소를 선택할 경우 해당 데이터가 전달되어 v-model에 적용됨으로써 알맞게 동작하게 된다.
  • 결과는 다음과 같다.


 

GitHub - Baeg-won/Baeg-won-Clothing-Gallery

Contribute to Baeg-won/Baeg-won-Clothing-Gallery development by creating an account on GitHub.

github.com

저작자표시 (새창열림)

'🚗 Backend Toy Project > Baeg-won Clothing Gallery' 카테고리의 다른 글

[Baeg-won Clothing Gallery] 8. WISH 페이지 구현  (0) 2023.07.28
[Baeg-won Clothing Gallery] 7. CART 페이지 구현  (0) 2023.07.24
[Baeg-won Clothing Gallery] 6. DETAIL 페이지 구현  (0) 2023.07.16
[Baeg-won Clothing Gallery] 5. SALE, CONTACT 페이지 구현  (0) 2023.07.15
[Baeg-won Clothing Gallery] 4. CLOTHING, FOOTWEAR, ACCESSORIES 페이지 구현  (0) 2023.07.14
'🚗 Backend Toy Project/Baeg-won Clothing Gallery' 카테고리의 다른 글
  • [Baeg-won Clothing Gallery] 8. WISH 페이지 구현
  • [Baeg-won Clothing Gallery] 7. CART 페이지 구현
  • [Baeg-won Clothing Gallery] 6. DETAIL 페이지 구현
  • [Baeg-won Clothing Gallery] 5. SALE, CONTACT 페이지 구현
Baeg-won
Baeg-won
  • Baeg-won
    좋았다면 추억이고 나빴다면 경험이다.
    Baeg-won
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 🍃 Spring, Spring Boot
        • 스프링 프레임워크 기초
        • 스프링 핵심 원리 - 기본편
        • 자바 ORM 표준 JPA 프로그래밍 - 기본편
        • 스프링 MVC
        • 실전! 스프링 부트와 JPA 활용1 - 웹 애플리..
      • 🥑 Web Technoloy
      • 🚗 Backend Toy Project
        • 스프링 부트 게시판
        • Photogram
        • Baeg-won Clothing Gallery
      • 🥇 Problem Solving
        • Breadth-First Search
        • Depth-First Search
        • Backtracking
        • Simulation
        • Two-pointer
        • Binary Search
        • Greedy
        • Dynamic Programming
        • Minimum Spanning Tree
        • Dijkstra
        • Floyd warshall
      • ☕ Java
        • 명품 자바 에센셜
        • Applications
      • 🍦 JavaScript
        • JavaScript 기초
      • 🐧 Linux
        • 이것이 리눅스다(CentOS 8)
      • 📟 Database
        • 혼자 공부하는 SQL
      • 🧬 Data Structure
      • 🎬 HTML
      • 🎤 Tech Interview
      • 📌 etc
        • Unity 2D Raising Jelly Game
        • C++
        • 영어 쉐도잉
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Baeg-won
[Baeg-won Clothing Gallery] 9. PROFILE 페이지 구현
상단으로

티스토리툴바