개발일지

Personal Project - 가계부 구현(2)

준서이 2024. 5. 8. 14:19

 

 

AccountBookDTO

package org.example.account_book.DTO;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.example.account_book.Constant.AccountRole;
import org.example.account_book.Constant.AccountType;

import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AccountBookDTO {

    //가계부 번호
    private Long accountId;

    //금액
    @NotBlank
    private Long money;

    //내용
    @NotBlank
    private String content;

    //분류(수입, 지출)
    @NotBlank
    private AccountRole accountRole;

    //지출 타입(카드, 현금, 이체)
    @NotBlank
    private AccountType accountType;

    //거래일
    @NotBlank
    private String date;

    //회원 정보
    private Long memberId;

    //등록일
    private LocalDateTime createdDate;

    //수정일
    private LocalDateTime modifiedDate;
}

 

 

 

 

 

AccountBookEntity

package org.example.account_book.Entity;

import jakarta.persistence.*;
import lombok.*;
import org.example.account_book.Constant.AccountRole;
import org.example.account_book.Constant.AccountType;

@Getter
@Setter
@ToString
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Table(name = "account_book")
@SequenceGenerator(name = "account_book_SEQ", sequenceName = "account_book_SEQ", allocationSize = 1)
public class AccountBookEntity extends BaseEntity{

    //가계부 번호
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "account_book_SEQ")
    @Column
    private Long accountId;

    //금액
    @Column
    private Long money;

    //내용
    @Column
    private String content;

    //거래일
    @Column
    private String date;

    //내역 분류(수입, 지출)
    @Enumerated(EnumType.STRING)
    private AccountRole accountRole;

    //지출 타입(카드, 현금, 이체)
    @Enumerated(EnumType.STRING)
    private AccountType accountType;

    //회원 정보
    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
    @JoinColumn(name = "member_id", nullable = false)
    private MemberEntity memberEntity;

}

 

 

 

 

 

AccountBookService

package org.example.account_book.Service;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.example.account_book.Constant.AccountRole;
import org.example.account_book.Constant.AccountType;
import org.example.account_book.DTO.AccountBookDTO;
import org.example.account_book.Entity.AccountBookEntity;
import org.example.account_book.Entity.MemberEntity;
import org.example.account_book.Repository.AccountBookRepository;
import org.example.account_book.Repository.MemberRepository;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.*;

@Service
@RequiredArgsConstructor
@Transactional
public class AccountBookService {
    private final ModelMapper modelMapper;

    private final AccountBookRepository accountBookRepository;

    private final MemberRepository memberRepository;

    //삽입
    public void save(AccountBookDTO accountBookDTO, Long memberId) {
        //회원 조회
        MemberEntity member = memberRepository.findById(memberId).orElseThrow();

        //회원 정보를 저장
        AccountBookEntity accountBookEntity = modelMapper.map(accountBookDTO, AccountBookEntity.class);
        accountBookEntity.setMemberEntity(member);
        accountBookRepository.save(accountBookEntity);
    }

    //수정
    public void update(AccountBookDTO accountBookDTO, Long memberId) {
        //회원 조회
        MemberEntity member = memberRepository.findById(memberId).orElseThrow();

        AccountBookEntity accountBookEntity = modelMapper.map(accountBookDTO, AccountBookEntity.class);
        accountBookEntity.setMemberEntity(member);
        accountBookRepository.save(accountBookEntity);

    }

    //삭제
    public void delete(Long accountBookId) {
        accountBookRepository.deleteById(accountBookId);
    }

    //전체조회
    public List<AccountBookDTO> getaccountBookList(Long memberId,
                                                   @RequestParam("type") String type,
                                                   @RequestParam("keyword") String keyword) {
        //가계부 조회
        List<AccountBookEntity> accountBook;
        //내용에서 키워드 가지고 검색
        if (type.equals("c") && keyword != null) {
            accountBook = accountBookRepository.findByContentContaining(keyword);
            //수입 내역 조회
        } else if (type.equals("i") && keyword.equals("INCOMES")) {
            accountBook = accountBookRepository.findByAccountRole(AccountRole.valueOf(keyword));
            //지출 내역 조회
        } else if (type.equals("e") && keyword.equals("EXPENSES")) {
            accountBook = accountBookRepository.findByAccountRole(AccountRole.valueOf(keyword));
            //카드 내역 조회
        } else if (type.equals("cd") && keyword.equals("CARD")) {
            accountBook = accountBookRepository.findByAccountType(AccountType.valueOf(keyword));
            //현금 내역 조회
        } else if (type.equals("ch") && keyword.equals("CASH")) {
            accountBook = accountBookRepository.findByAccountType(AccountType.valueOf(keyword));
            //은행 거래 내역 조회
        } else if (type.equals("b") && keyword.equals("BANK")) {
            accountBook = accountBookRepository.findByAccountType(AccountType.valueOf(keyword));
        } else {
            accountBook = accountBookRepository.findAll();
        }

        List<AccountBookDTO> accountBookDTO;

        //회원의 가계부 일 때 변환
        if (accountBook.get(0).getMemberEntity().getMemberId().equals(memberId)) {
            accountBookDTO = Arrays.asList(modelMapper.map(accountBook, AccountBookDTO[].class));
        } else {
            //조회한 내역이 없을 경우 예외발생 처리
            throw new RuntimeException("The member's account book list is empty.");
        }

        return accountBookDTO;
    }

    //개별조회
    public AccountBookDTO findById(Long accountBookId, Long memberId) {
        AccountBookEntity accountBook = accountBookRepository.findById(accountBookId).orElseThrow();

        AccountBookDTO accountBookDTO = null;
        //회원의 가계부 일 때 변환
        if (accountBook.getMemberEntity().getMemberId().equals(memberId)) {
            accountBookDTO = modelMapper.map(accountBook, AccountBookDTO.class);
        }

        return accountBookDTO;
    }

    //월별 조회
    public List<AccountBookDTO> getMonth(@RequestParam(value = "date") String date,
                                         Long memberId) {
        //가계부 조회
        List<AccountBookEntity> accountBook;

        //거래일(년,월) 기준으로 조회
        if (date != null) {
            accountBook = accountBookRepository.findByDateContaining(date);
        } else {
            accountBook = accountBookRepository.findAll();
        }

        List<AccountBookDTO> accountBookDTO;

        //회원의 가계부 일 때 변환
        if (accountBook.get(0).getMemberEntity().getMemberId().equals(memberId)) {
            accountBookDTO = Arrays.asList(modelMapper.map(accountBook, AccountBookDTO[].class));
        } else {
            //조회한 내역이 없을 경우 예외발생 처리
            throw new RuntimeException("The member's account book list is empty.");
        }

        return accountBookDTO;
    }

    public Long income(List<AccountBookDTO> accountBookDTO) {
        //수입 총액구하기
        Long income = 0L;

        for (AccountBookDTO bookDTO : accountBookDTO) {
            // Check if the account role is INCOMES
            if (bookDTO.getAccountRole().name().equals("INCOMES")) {
                // Add money to income
                income += bookDTO.getMoney();
            }
        }

        return income;
    }

    public Long expense(List<AccountBookDTO> accountBookDTO) {
        //지출 총액 구하기
        Long expense = 0L;

        for (AccountBookDTO bookDTO : accountBookDTO) {
            // Check if the account role is EXPENSES
            if (bookDTO.getAccountRole().name().equals("EXPENSES")) {
                // Add money to expense
                expense += bookDTO.getMoney();
            }
        }

        return expense;
    }

}

 

@Transactional
모든 작업들이 성공적으로 완료되어야 작업 묶음의 결과를 적용하고, 어떤 작업에서 오류가 발생했을 때는 이전에 있던 모든 작업들이 성공적이었더라도 없었던 일처럼 완전히 되돌리는 것이 트랜잭션의 개념이다.
데이터베이스를 다룰 때 트랜잭션을 적용하면 데이터 추가, 수정, 삭제 등으로 이루어진 작업을 처리하던 중 오류가 발생했을 때 모든 작업들을 원상태로 되돌릴 수 있다. 모든 작업들이 성공해야만 최종적으로 데이터베이스에 반영하도록 한다.
@Transactional이 붙은 메서드는 메서드가 포함하고 있는 작업 중에 하나라도 실패할 경우 전체 작업을 취소한다.

orElseThrow()
orElseThrow는 Optional의 인자가 null일 경우 예외처리를 시킨다.
조회한 값이 null일 경우 예외를 발생시키며,
null이 아닐 경우 내부적으로 Optional의 value를 가져오도록 구현되어있다.

@RequiredArgsConstructor
생성자 자동 생성 및 final 변수를 의존관계를 자동으로 설정해 준다.

 

 

 

AccountBookController

package org.example.account_book.Controller;

import lombok.RequiredArgsConstructor;
import org.example.account_book.DTO.AccountBookDTO;
import org.example.account_book.DTO.MemberDTO;
import org.example.account_book.Service.AccountBookService;
import org.example.account_book.Service.MemberService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.List;

@Controller
@RequiredArgsConstructor
public class AccountBookController {
    private final AccountBookService accountBookService;
    private final MemberService memberService;

    //삽입
    //로그인 한 경우에만 실행
    @PreAuthorize("isAuthenticated()")
    @GetMapping("/account/save")
    public String saveForm(Authentication authentication, Model model) {

        MemberDTO memberDTO = memberService.findByEmail(authentication.getName());

        model.addAttribute("member", memberDTO);

        return "accountbook/save";
    }

    //로그인 한 경우에만 실행
    @PreAuthorize("isAuthenticated()")
    @PostMapping("/account/save")
    public String saveProcess(AccountBookDTO accountBookDTO, Authentication authentication,
                              RedirectAttributes redirectAttributes) {

        //로그인한 회원의 정보를 읽어온다
        MemberDTO memberDTO = memberService.findByEmail(authentication.getName());

        //날짜 형식 유효성 검사
        String dateString = accountBookDTO.getDate();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        try {
            dateFormat.parse(dateString);
        } catch (ParseException e) {
            redirectAttributes.addFlashAttribute("errorMessage", "날짜 형식이 올바르지 않습니다. (yyyy-MM-dd)");
            return "redirect:/account/save";
        }

        //인증 된 회원일 때 저장
        if (memberDTO != null) {
            accountBookService.save(accountBookDTO, memberDTO.getMemberId());
        }

        redirectAttributes.addFlashAttribute("successMessage",
                "게시글이 등록되었습니다.");

        System.out.println(memberDTO);
        System.out.println(accountBookDTO);

        return "redirect:/account/list";
    }

    //수정
    //로그인 한 경우에만 실행
    @PreAuthorize("isAuthenticated()")
    @GetMapping("/account/update")
    public String updateForm(@RequestParam("id") Long id, Model model,
                             Authentication authentication,
                             RedirectAttributes redirectAttributes) {

        //로그인한 회원의 정보를 읽어온다
        MemberDTO memberDTO = memberService.findByEmail(authentication.getName());

        //가계부 조회
        AccountBookDTO accountBookDTO = accountBookService.findById(id, memberDTO.getMemberId());

        //가계부에 있는 회원 ID와 로그인한 회원의 ID가 일치하지 않으면
        if (!accountBookDTO.getMemberId().equals(memberDTO.getMemberId())) {
            redirectAttributes.addFlashAttribute("error",
                    "회원 정보가 일치하지 않습니다.");
            return "redirect:/account/list";
        }

        model.addAttribute("data", accountBookDTO);

        System.out.println(memberDTO);
        System.out.println(accountBookDTO);

        return "accountbook/update";
    }

    //로그인 한 경우에만 실행
    @PreAuthorize("isAuthenticated()")
    @PostMapping("/account/update")
    public String updateProcess(AccountBookDTO accountBookDTO,
                                Authentication authentication,
                                RedirectAttributes redirectAttributes) {
        //로그인한 회원의 정보를 읽어온다
        MemberDTO memberDTO = memberService.findByEmail(authentication.getName());

        //날짜 형식 유효성 검사
        String dateString = accountBookDTO.getDate();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        try {
            dateFormat.parse(dateString);
        } catch (ParseException e) {
            redirectAttributes.addFlashAttribute("errorMessage", "날짜 형식이 올바르지 않습니다. (yyyy-MM-dd)");
            return "redirect:/account/update";
        }

        //인증 된 회원일 때 수정
        if (memberDTO != null) {
            accountBookService.update(accountBookDTO, memberDTO.getMemberId());
        }

        redirectAttributes.addFlashAttribute("successMessage",
                "게시글이 수정되었습니다.");

        System.out.println(accountBookDTO);
        return "redirect:/account/list";
    }

    //삭제
    //로그인 한 경우에만 실행
    @PreAuthorize("isAuthenticated()")
    @GetMapping("/account/delete")
    public String deleteProcess(Long id, Authentication authentication,
                                RedirectAttributes redirectAttributes) {

        //로그인한 회원의 정보를 읽어온다
        MemberDTO memberDTO = memberService.findByEmail(authentication.getName());
        //가계부 조회
        AccountBookDTO accountBookDTO = accountBookService.findById(id, memberDTO.getMemberId());

        //가계부에 있는 회원 ID와 로그인한 회원의 ID가 일치하면 삭제
        if (accountBookDTO.getMemberId().equals(memberDTO.getMemberId())) {
            accountBookService.delete(id);
        } else {
            redirectAttributes.addFlashAttribute("error", "권한이 없습니다.");
            return "redirect:/account/list";
        }

        redirectAttributes.addFlashAttribute("successMessage",
                "게시글이 삭제되었습니다.");

        System.out.println(accountBookDTO);
        return "redirect:/account/list";
    }

    //전체조회
    @GetMapping("/account/list")
    public String accountBookList(Model model, Authentication authentication,
                                  @RequestParam(value = "type", defaultValue = "") String type,
                                  @RequestParam(value = "keyword", defaultValue = "") String keyword) {

        //로그인한 회원의 정보를 읽어온다
        MemberDTO memberDTO = memberService.findByEmail(authentication.getName());

        List<AccountBookDTO> accountBookDTO = accountBookService.getaccountBookList(memberDTO.getMemberId(), type, keyword);

        //수입 총액
        Long income = accountBookService.income(accountBookDTO);
        //지출 총액
        Long expense = accountBookService.expense(accountBookDTO);
        //총 자산
        Long total = income - expense;

        //요소의 개수
        int length = 0;
        length += accountBookDTO.size();

        model.addAttribute("length", length);

        model.addAttribute("income", income);

        model.addAttribute("expense", expense);

        model.addAttribute("total", total);

        model.addAttribute("list", accountBookDTO);

        System.out.println(accountBookDTO);
        return "accountbook/list";
    }

    //월별조회
    @GetMapping("/account/month")
    public String monthList(Model model,
                            @RequestParam(value = "date", defaultValue = "") String date,
                            Authentication authentication) {

        //로그인한 회원의 정보를 읽어온다
        MemberDTO memberDTO = memberService.findByEmail(authentication.getName());

        List<AccountBookDTO> accountBookDTO = accountBookService.getMonth(date, memberDTO.getMemberId());

        //수입 총액
        Long income = accountBookService.income(accountBookDTO);
        //지출 총액
        Long expense = accountBookService.expense(accountBookDTO);
        //총 자산
        Long total = income - expense;

        //요소의 개수
        int length = 0;
        length += accountBookDTO.size();

        model.addAttribute("length", length);

        model.addAttribute("income", income);

        model.addAttribute("expense", expense);

        model.addAttribute("total", total);

        model.addAttribute("list", accountBookDTO);

        model.addAttribute("month", accountBookDTO.get(0).getDate().substring(5, 7));

        System.out.println(date);
        System.out.println(accountBookDTO);
        return "accountbook/month";
    }

    //개별조회
    @GetMapping("/account/detail")
    public String accountBookDetail(Long id, Authentication authentication, Model model) {

        //로그인한 회원의 정보를 읽어온다
        MemberDTO memberDTO = memberService.findByEmail(authentication.getName());

        AccountBookDTO accountBookDTO = accountBookService.findById(id, memberDTO.getMemberId());

        model.addAttribute("data", accountBookDTO);

        System.out.println(accountBookDTO);
        return "accountbook/detail";
    }

}

 

@RequiredArgsConstructor
생성자 자동 생성 및 final 변수를 의존관계를 자동으로 설정해 준다.

@GetMapping
@RequestMapping(Method=RequestMethod.GET)과 같다.

@PostMapping
@RequestMapping(Method=RequestMethod.POST)과 같다.

날짜 형식 유효성 검사
읽어온 날짜의 값이 yyyy-MM-dd 형식이 아닐 경우 예외 발생 처리하였다.
redirectAttributes.addFlashAttribute로 에러 메세지를 보내고, redirect로 삽입 페이지로 리턴하였다.

 

 

 

save.html

<!DOCTYPE html>
<html lang="ko"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
      layout:decorate="~{layouts/main}">
<head>
    <meta charset="UTF-8">
    <title>AccountBook</title>
    <!--  bootstrap  -->
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
    <!--  google font  -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Single+Day&display=swap" rel="stylesheet">
    <style>
        .row {
            font-family: "Single Day", cursive;
            font-weight: 500;
            font-style: normal;
            font-size: 15px;
            display: flex;
            justify-content: center;
            text-align: center;
            padding: 20px;
            margin: auto;
            background-color: ivory;
        }

        .button {
            background-color: #f4511e;
            border: none;
            color: white;
            width: 100%;
            padding: 10px 32px;
            text-align: center;
            font-size: 16px;
            margin: 10px 0;
            opacity: 0.3;
            transition: 0.3s;
            display: inline-block;
            text-decoration: none;
            cursor: pointer;
        }

        .button:hover {
            opacity: 1
        }

    </style>
</head>
<body>
<div layout:fragment="content">
    <div class="row">
        <!-- 여백 -->
        <div class="col-sm-3"></div>
        <!-- 내용 -->
        <div class="col-sm-6">
            <div class="container mt-5 mb-5 p-5 my-5">
                <div class="container mt-3">
                    <form th:action="@{/account/save}" method="post"
                          onsubmit="return validateForm()">
                        <div class="mb-3 mt-3">
                            <label for="date">거래일</label>
                            <input type="text" class="form-control" id="date" name="date" pattern="yyyy-MM-dd"
                                   title="날짜 형식에 맞게 입력해주세요." placeholder="yyyy-MM-dd" required>
                        </div>
                        <div class="mb-3 mt-3">
                            <label for="money">금액</label>
                            <input type="number" class="form-control" id="money" placeholder="only numbers"
                                   name="money" title="숫자만 입력해주세요." required>
                        </div>
                        <div class="mb-3">
                            <label for="content">내용</label>
                            <input type="text" class="form-control" id="content" placeholder="content"
                                   name="content" title="내용을 입력해주세요." required>
                        </div>
                        <div class="mb-3 mt-3">
                            <label for="accountType" class="form-label">거래방식</label>
                            <select class="form-select" id="accountType" name="accountType" required>
                                <option value="CARD">CARD</option>
                                <option value="CASH">CASH</option>
                                <option value="BANK">BANK</option>
                            </select>
                        </div>
                        <div class="mb-3 mt-3">
                            <label for="accountRole" class="form-label">수입/지출</label>
                            <select class="form-select" id="accountRole" name="accountRole" required>
                                <option value="INCOMES">INCOMES</option>
                                <option value="EXPENSES">EXPENSES</option>
                            </select>
                        </div>
                        <button type="submit" class="button" sec:authorize="hasAnyRole('USER', 'ADMIN')">
                            Submit
                        </button>
                    </form>
                </div>
            </div>
        </div>
        <!-- 여백 -->
        <div class="col-sm-3"></div>
    </div>
</div>
<th:block layout:fragment="script">
    <script th:inline="javascript">
        /* 작업 성공했을 때 성공메세지 창을 출력 */
        var successMessage = /*[[ ${successMessage} ]]*/ null;
        if (successMessage) {
            alert(successMessage);
        }
    </script>

    <script th:inline="javascript">
        /* 에러가 있을 때 에러메세지 창을 출력 */
        var errorMessage = /*[[ ${errorMessage} ]]*/ null;
        if (errorMessage) {
            alert(errorMessage);
        }
    </script>

    <script th:inline="javascript">
        function validateForm() {
            if (validateDate() && validateNumber()) {
                return true; // 폼 제출
            } else {
                return false; // 폼 제출 방지
            }
        }

        // keydown 이벤트 사용
        document.getElementById('money').addEventListener('keydown', function(e) {
            if (isNaN(parseInt(e.key)) && e.key !== 'Backspace' && e.key !== 'Delete') {
                e.preventDefault();
            }
        });

        // oninput 이벤트 사용
        document.getElementById('money').oninput = function() {
            this.value = this.value.replace(/[^0-9]/g, '');
        };

        //정규식을 사용하여 숫자만 입력 허용
        function validateNumber(event) {
            var key =  event.keyCode || event.which;
            if (event.keyCode === 8 || event.keyCode === 46 || event.keyCode === 37 || event.keyCode === 39) {
                return true;
            } else return !(key < 48 || key > 57);
        }

        //데이터 유형에 따라 날짜 값의 유효성을 검사
        function validateDate() {
            const dateInput = document.getElementById('date');
            const dateValue = dateInput.value.trim();

            const monthRegex = /^\d{4}-\d{2}-\d{2}$/;
            if (!monthRegex.test(dateValue)) {
                alert('Please enter a valid date in the format "yyyy-MM-dd". Thank you♥');
                return false;
            }

            return true;
        }
    </script>
</th:block>
</body>
</html>

 

날짜 형식 유효성 검사
keydown 이벤트를 사용하여 지정된 코드 이회의 키는 입력을 제한하였으며,
정규식을 사용하여 데이터 형식에 맞는 값을 입력하도록 지정하여 날짜 값의 유효성 검사를 하였으며, 형식이 맞지 않을 경우 폼 제출을 방지하는 코드를 작성하였다.

required
속성을 추가하여 필수 값으로 입력할 수 있게 작성하였다.

 

 

 

 

 

list.html

<!DOCTYPE html>
<html lang="ko"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
      layout:decorate="~{layouts/main}">
<head>
    <meta charset="UTF-8">
    <title>AccountBook</title>
    <!--  bootstrap  -->
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
    <!--  google icon  -->
    <link rel="stylesheet"
          href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0"/>
    <!--  google font  -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Gaegu&display=swap" rel="stylesheet">
    <link href="https://fonts.googleapis.com/css2?family=Single+Day&display=swap" rel="stylesheet">
    <style>
        .row {
            font-family: "Single Day", cursive;
            font-weight: 500;
            font-style: normal;
            font-size: 15px;
            display: flex;
            justify-content: center;
            text-align: center;
            padding: 20px;
            margin: auto;
            background-color: ivory;
        }

        .card {
            text-align: left;
            border-left: 7px solid black;
            border-right: 7px solid black;
            margin-bottom: 30px;
            height: 80px;
        }

        .card1 {
            font-family: "Gaegu", sans-serif;
            font-weight: 700;
            font-size: 20px;
            text-align: center;
            width: 200px;
            height: 170px;
            border-radius: 15px;
            border: 3px solid black;
            margin: 10px;
            padding: 15px;
        }

        .search {
            width: 50%;
            margin-left: 25%;
        }

        #type, #datatype {
            border: 3px solid black;
        }

        #keyword, #date {
            border: 3px solid black;
        }

        .button {
            border: none;
            background-color: white;
        }

        .cardbody {
            container: flex;
            padding-top: 30px;
            color: #faa589;
        }

        .list {
            text-align: left;
            font-size: 12px;
            color: #faa589;
        }

    </style>
</head>
<body>
<div layout:fragment="content">
    <div class="row">
        <!-- 여백 -->
        <div class="col-sm-2"></div>
        <!-- 내용 -->
        <div class="col-sm-5">
            <div class="container mt-3 mb-3 p-3 my-3">
                <div class="container mt-3 mb-5">
                    <form class="d-flex" th:action="@{/account/month}" method="get"
                          onsubmit="return validateDate()">
                        <div class="container input-group mb-3 mt-3">
                            <label for="datatype" class="form-label"></label>
                            <select class="form-select-sm" id="datatype" name="datatype" onchange="changePlaceholder()">
                                <option value="year">year</option>
                                <option value="month">month</option>
                            </select>
                            <label for="date" class="form-label"></label>
                            <input type="text" class="form-control" name="date" id="date"
                                   th:value="${param.date}" placeholder="yyyy">
                            <button type="submit" class="btn">
                                <span class="material-symbols-outlined">
                                    search
                                </span>
                            </button>
                        </div>
                    </form>
                </div>
                <div class="list container mt-3 mb-3">
                    <span>
                         전체 내역 :
                    </span>
                    <span th:text="${length}"/>
                </div>
                <div class="container mt-3" th:each="data:${list}">
                    <div class="card">
                        <div class="card-body">
                            <table class="table table-borderless">
                                <tbody>
                                <tr>
                                    <td th:text="${data.accountId}">
                                        accountId
                                    </td>
                                    <td th:text="${data.date}">
                                        date
                                    </td>
                                    <td th:text="${data.money}">
                                        money
                                    </td>
                                    <td th:text="${data.content}">
                                        content
                                    </td>
                                    <td th:text="${data.accountType.name()}">
                                        AccountType
                                    </td>
                                    <td th:text="${data.accountRole.name()}">
                                        AccountRole
                                    </td>
                                    <td>
                                        <button type="button" class="button"
                                                th:onclick="|location.href='@{/account/update(id=${data.accountId})}'|"
                                                sec:authorize="hasAnyRole('USER', 'ADMIN')">
                                            <span class="material-symbols-outlined">
                                                edit_note
                                            </span>
                                        </button>
                                    </td>
                                    <td>
                                        <button type="button" class="button"
                                                th:onclick="|confirmDelete(${data.accountId})|"
                                                sec:authorize="hasAnyRole('USER', 'ADMIN')">
                                            <span class="material-symbols-outlined">
                                                delete
                                            </span>
                                        </button>
                                    </td>
                                </tr>
                                </tbody>
                            </table>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div class="col-sm-3">
            <div class="container mt-3 mb-3 p-3 my-3">
                <div class="container mt-3 mb-5">
                    <div class="card1">
                        <div class="card-body">총 자산</div>
                        <div class="cardbody" th:text="${total}">
                        </div>
                    </div>
                </div>
                <div class="container mt-3 mb-5">
                    <div class="card1">
                        <div class="card-body">수입</div>
                        <div class="cardbody" th:text="${income}">
                        </div>
                    </div>
                </div>
                <div class="container mt-3 mb-5">
                    <div class="card1">
                        <div class="card-body">지출</div>
                        <div class="cardbody" th:text="${expense}">
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div class="search">
            <form class="d-flex" th:action="@{/account/list}" method="get"
                  onsubmit="return submitForm()">
                <div class="container input-group mb-3 mt-3">
                    <label for="type" class="form-label"></label>
                    <select class="form-select-sm" id="type" name="type">
                        <option value="" th:selected="${type==null}" hidden=""></option>
                        <option value="c" th:selected="${type=='c'}">내용</option>
                        <option value="i" th:selected="${type=='i'}">수입</option>
                        <option value="e" th:selected="${type=='e'}">지출</option>
                        <option value="cd" th:selected="${type=='cd'}">카드</option>
                        <option value="ch" th:selected="${type=='ch'}">현금</option>
                        <option value="b" th:selected="${type=='b'}">은행</option>
                    </select>
                    <label for="keyword" class="form-label"></label>
                    <input type="text" class="form-control" name="keyword"
                           id="keyword" th:value="${param.keyword}"
                           placeholder=" "/>
                    <button type="submit" class="btn">
                                <span class="material-symbols-outlined">
                                    search
                                </span>
                    </button>
                </div>
            </form>
        </div>
        <!-- 여백 -->
        <div class="col-sm-2"></div>
    </div>
</div>
<th:block layout:fragment="script">
    <script th:inline="javascript">
        /* 작업 성공했을 때 성공메세지 창을 출력 */
        var successMessage = /*[[ ${successMessage} ]]*/ null;
        if (successMessage) {
            alert(successMessage);
        }
    </script>

    <script th:inline="javascript">
        // 페이지 로드 시 실행되는 함수
        window.onload = function () {
            // type 선택 드롭다운 메뉴 변경 시 실행되는 함수
            document.getElementById('type').addEventListener('change', function () {
                var typeSelect = document.getElementById('type');
                var keywordInput = document.getElementById('keyword');

                // type이 'c'일 때만 keyword 입력 가능하도록 설정
                if (typeSelect.value === 'c') {
                    keywordInput.placeholder = 'Please enter your details :)';
                    keywordInput.readOnly = false;
                } else {
                    keywordInput.placeholder = ':)';
                    keywordInput.readOnly = true;
                }
            });
        };

        // 폼 제출 시 실행되는 함수
        function submitForm() {
            var typeSelect = document.getElementById("type");
            var keywordInput = document.getElementById("keyword");

            // 선택된 옵션 값에 따라 keyword 값 설정
            switch (typeSelect.value) {
                case "i":
                    keywordInput.value = "INCOMES";
                    break;
                case "e":
                    keywordInput.value = "EXPENSES";
                    break;
                case "cd":
                    keywordInput.value = "CARD";
                    break;
                case "ch":
                    keywordInput.value = "CASH";
                    break;
                case "b":
                    keywordInput.value = "BANK";
                    break;
                default:
                    keywordInput.value = "";
                    break;
            }

            // 폼 제출
            document.querySelector("form").submit();
        }
    </script>

    <script th:inline="javascript">
        //데이터 유형에 따라 placeholder 변경
        function changePlaceholder() {
            const dateInput = document.getElementById('date');
            const datatype = document.getElementById('datatype').value;

            if (datatype === 'year') {
                dateInput.placeholder = 'yyyy';
            } else {
                dateInput.placeholder = 'yyyy-MM';
            }
        }

        //데이터 유형에 따라 날짜 값의 유효성을 검사
        function validateDate() {
            const dateInput = document.getElementById('date');
            const datatype = document.getElementById('datatype').value;
            const dateValue = dateInput.value.trim();

            if (datatype === 'year') {
                const yearRegex = /^\d{4}$/;
                if (!yearRegex.test(dateValue)) {
                    alert('Please enter a valid year in the format "yyyy". Thank you♥');
                    return false;
                }
            } else {
                const monthRegex = /^\d{4}-\d{2}$/;
                if (!monthRegex.test(dateValue)) {
                    alert('Please enter a valid month in the format "yyyy-MM". Thank you♥');
                    return false;
                }
            }

            return true;
        }
    </script>

    <script th:inline="javascript">
        //게시글 삭제 시 더블 체크
        function confirmDelete(accountId) {
            if (window.confirm("이 게시글을 정말로 삭제하시겠습니까?")) {
                // 서버로 삭제 요청 보내기
                fetch("/account/delete?id=" + accountId, {
                    method: 'DELETE'
                })
                    .then(response => {
                        if (response.ok) {
                            //삭제 성공 시 사용자에게 메시지 표시
                            alert("게시글이 성공적으로 삭제되었습니다.");
                            //삭제 후 리디렉션
                            window.location.href = "/account/list";
                        } else {
                            //삭제 실패 시 사용자에게 메시지 표시
                            alert("게시글 삭제 중 오류가 발생했습니다. 다시 시도해주세요.");
                        }
                    })
                    .catch(error => {
                        //네트워크 오류 등 예기치 못한 오류 발생 시 사용자에게 메시지 표시
                        alert("게시글 삭제 중 오류가 발생했습니다. 다시 시도해주세요.");
                        console.error('Error:', error);
                    });
            } else {
                //사용자가 취소를 누른 경우
                //아무 작업도 하지 않음
            }
        }
    </script>
</th:block>
</body>
</html>

 

게시글 검색 시,
타입 유형에 따라 placeholder를 변경하여 적용하였으며,
내용을 선택했을 경우에만 키워드를 입력할 수 있게 설정하고,

그 이외에 수입, 지출, 카드, 현금, 은행을 선택했을 때는 키워드 값을 각 타입에 맞게 지정된 값으로 변경하고 폼을 제출 할 수 있게 설정하였다.

월 별 조회 시,
년도별, 년도+월별 선택에 따라 placeholder를 변경하여 적용하였으며,
데이터 형식에 맞는 값을 입력하도록 지정하여 날짜 값의 유효성 검사를 하였으며, 형식이 맞지 않을 경우 폼 제출을 방지하는 코드를 작성하였다.

게시글 삭제 시 중복 체크
혹여나 잘못 눌렀을 경우, 실수를 방지하기 위해 회원 탈퇴 시 확인 여부를 한 번 더 체크하는 script를 작성하였다.

 

 

 

 

화면 캡쳐

list page(year를 선택했을 때 placeholder='yyyy'로 변경)

 

 

list page(month를 선택했을 때 placeholder='yyyy-MM'으로 변경)

 

 

list page(내용을 선택했을 때 placeholder 변경)

 

 

list page(수입을 선택했을 때 placeholder 및 input을 readonly로 변경)

 

 

update page