개발자꿈나무

FureverHomes 프로젝트 - 입양 신청 & 관심 동물 등록 본문

기술블로그

FureverHomes 프로젝트 - 입양 신청 & 관심 동물 등록

망재이 2023. 10. 4. 10:09
1. 입양 신청

 

 

 

 

AdoptReqDTO

 

 

입양 시 입력받는 정보들을 DTO로 만들어준다.

@Getter
public class AdoptReqDTO {
    private Long memberId;
    private Long animalId;
    private String phonenum;
    private String contact_time;
    private String residence;
    private String job;
    private Boolean breeding;
    private String adopt_reason;
    private String add_comment;
    private AdoptStatus adopt_status;

    public Adopt toEntity(Member member, Animal animal) {
        return Adopt.builder()
                .member(member)
                .animal(animal)
                .phonenum(phonenum)
                .contact_time(contact_time)
                .residence(residence)
                .job(job)
                .breeding(breeding)
                .adopt_reason(adopt_reason)
                .add_comment(add_comment)
                .adopt_status(AdoptStatus.PROCEEDING)
                .build();
    }

    public void updateMemberId(Long memberId) {
        this.memberId = memberId;
    }
}

Adopt 엔티티를 보면 Animal, Member 엔티티와 연관관계를 맺고있는데 DTO는 각각의 Id 값만 불러오고 빌더를 이용해서 DTO를 엔티티로 변환해줄때 Member, Animal 객체를 넣어주는 방법을 택했다. 그리고 회원의 아이디는 서버 세션에서 관리하기 때문에 DTO에 memberId를 넣어주기 위해 updateMemberId 메소드를 생성했다.

 

 

 

 

AdoptRepository

 

 

@Repository
public interface AdoptRepository extends JpaRepository<Adopt, Long> {
    Boolean existsByAnimalAndMember(Animal animal, Member member); //중복 등록 여부 확인
}

입양 신청을 할 때 중복 등록이 되어있는지를 확인하기 위해서 existsByAnimalAndMember 메소드를 생성했다. JPA에서는 값을 삽입할 때 알아서 중복 등록 여부를 확인하고 예외를 던져주기 때문에 ExceptionHandler를 이용해서 클라이언트에게 오류 메세지를 던져주는 방법도 존재한다. 하지만 나는 핸들러를 사용하지 않고 직접 메소드를 이용해서 확인하는 방법을 택했다.

더보기

[ExceptionHandler 사용 예시]

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(DataIntegrityViolationException.class)
    public ResponseEntity<String> handleDataIntegrityViolation(DataIntegrityViolationException e) {
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body("중복된 값이 이미 존재합니다.");
    }
}

이렇게 설정을 해주면 DataIntegrityViolationException이 발생했을 때 클라이언트에 BAD_REQUEST 상태와 함께 메세지를 반환한다고 한다. 이렇게 예외를 처리하면 트랜잭션이 롤백되고 예외를 클라이언트에게 바로 전송할 수 있을 것이다.

 

 

 

 

AdoptService

 

 

//입양 신청
@Transactional
public Boolean insertAdopt(final AdoptReqDTO adoptReqDTO) {
    Animal animal = animalRepository.findById(adoptReqDTO.getAnimalId()).orElseThrow(() -> new EntityNotFoundException("Animal 객체가 없음"));
    Member member = memberRepository.findById(adoptReqDTO.getMemberId()).orElseThrow(() -> new EntityNotFoundException("Member 객체가 없음"));
    if (isExistAdopt(animal, member)) { //같은 입양 내역 존재
        return false;
    }
    adoptRepository.save(adoptReqDTO.toEntity(member, animal));
    return true;
}

//입양 내역이 존재하는지 확인
private boolean isExistAdopt(final Animal animal, final Member member) {
    return adoptRepository.existsByAnimalAndMember(animal, member);
}

그냥 findById를 사용하면 Optional 객체를 반환하기 때문에 엔티티를 편리하게 로딩하기 위해 orElseThrow 메소드를 이용해서 객체가 없으면 예외를 발생시키도록 만들었다. 입양 내역이 존재하면 false를 반화하고 존재하지 않으면 저장 후 true를 반환하도록 하였다.

 

 

AdoptController

 

 

//입양 신청
@PostMapping("/detail/{animal_id}/adopt")
public ResponseEntity<Object> insertAdopt(@RequestBody AdoptReqDTO adoptReqDTO, HttpServletRequest request) {
    HttpSession session = request.getSession();
    adoptReqDTO.updateMemberId((Long) session.getAttribute("loginMember"));
    if(adoptService.insertAdopt(adoptReqDTO)) return ResponseEntity.ok().build();
    return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}

세션에 있는 회원 아이디를 꺼내오고 DTO에 넣어준 후 service를 호출해준다. true일 때는 정상 응답 코드를 false일 때는 일치하는 객체가 있다는 의미로 NOT_FOUND 응답을 해줬다.

 

 

 

 

Adopt.js & html

 

 

<!-- 입양신청 -->
<div class="tab-pane fade" id="nav-booking" role="tabpanel" aria-labelledby="nav-booking-tab">
    <form class="ml-3" id="signInForm">
        <div class="row">
            <div class="col-md-6 mb-3">
                <div class="form-group">
                    <label for="phonenum">연락처
                        <span class="txt-error tel"
                            th:style="'padding-left:10px; font-size:13px;'"></span>
                    </label>
                    <input class="form-control" id="phonenum" type="text"
                    placeholder="숫자만 입력해주세요" autocomplete="off" required>
                </div>
            </div>
                <div class="col-md-6 mb-3">
                    <div class="form-group">
                        <label for="contact_time">연락가능시간<span class="txt-error time"
                            th:style="'padding-left:10px; font-size:13px;'"></span></label>
                        <input class="form-control" id="contact_time" type="text"
                            placeholder="예) 오후 3시" autocomplete="off" required>
                    </div>
                </div>
        	</div>
        <div class="row align-items-center">
            <div class="col-md-6 mb-3">
                <div class="form-group">
                    <label>거주지역</label>
                        <select class="custom-select" id="residence" required>
                            <option value="residence" disabled selected th:style="'display:none'">지역
                            </option>
                            <option value="SEOUL">서울</option>
                            <option value="INCHEON">인천</option>
                            <option value="DAEJEON">대전</option>
                            <option value="DAEGU">대구</option>
                            <option value="ULSAN">울산</option>
                            <option value="BUSAN">부산</option>
                            <option value="GWANGJU">광주</option>
                            <option value="SEJONG">세종</option>
                            <option value="GYEONGGI">경기도</option>
                            <option value="GANGWON">강원도</option>
                            <option value="CHUNGCHEONG">충청도</option>
                            <option value="GYEONGSANG">경상도</option>
                            <option value="JEOLLA">전라도</option>
                            <option value="JEJU">제주도</option>
                    </select>
                </div>
            </div>
            <div class="col-md-6 mb-3">
                <div class="form-group">
                    <label for="job">직업<span class="txt-error job"
                        th:style="'padding-left:10px; font-size:13px;'"></span></label>
                    <input class="form-control" id="job" type="text"
                        placeholder="간략하게 적어주세요" autocomplete="off">
                </div>
            </div>
        </div>
        <div class="form-group">
            <div class="mb-4"
            th:style="'position: relative; width: 100%; padding-right: 15px; padding-left: 0px;'">
                <label>동물을 키워본 이력</label>
                <div class="form-check">
                    <input class="form-check-input" type="radio" name="breeding"
                           id="breeding-Y" value="Y" checked>
                    <label class="form-check-label pr-2" for="breeding-Y">
                        존재한다
                    </label>
                    <input class="form-check-input" type="radio" name="breeding"
                           id="breeding-N" value="N">
                    <label class="form-check-label" for="breeding-N">
                        존재하지 않는다
                    </label>
                </div>
            </div>
        </div>
        <div class="row">
            <div class="col-sm-12 mb-3">
                <div class="form-group">
                    <label for="adopt_reason">입양을 원하는 이유</label>
                    <textarea class="form-control" id="adopt_reason" rows="3" placeholder="필수 입력 항목입니다." required></textarea>
                </div>
            </div>
        </div>
        <div class="row">
            <div class="col-sm-12 mb-3">
                <div class="form-group">
                    <label for="add_comment">덧붙이고 싶은 말</label>
                    <textarea class="form-control" id="add_comment" rows="5"></textarea>
                </div>
            </div>
        </div>
        <div class="mt-3">
            <!-- 버튼 클릭시 모달창 -->
            <!-- 모달 버튼-->
            <button type="button" id="reg-btn" class="btn btn-warning"
                    data-toggle="modal"
                    data-target="#modal-default-2"
                    th:style="'float: right'">
                신청하기
            </button>
            <!--모달 시작-->
            <div class="modal fade" id="modal-default-2" tabindex="-1" role="dialog"
                 aria-labelledby="modal-default" aria-hidden="true">
                <div class="modal-dialog modal-dialog-centered" role="document">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h6 class="modal-title">입양신청</h6>
                        </div>
                        <div class="modal-body">
                            <form>
                                <div class="mb-2">
                                    <label class="col-form-label">정말로 입양 신청하시겠습니까?</label>
                                </div>
                            </form>
                        </div>
                        <div class="modal-footer">
                            <button id="adopt-btn" type="button"
                                    class="btn btn-sm btn-warning"
                                    data-dismiss="modal">예, 신청합니다
                            </button>
                            <button type="button" id="close"
                                    class="btn btn-link text-danger ml-auto"
                                    data-dismiss="modal">아니요
                            </button>
                        </div>
                    </div>
                </div>
            </div>
            <!-- Modal End-->
        </div>
    </form>
    <!-- </form> -->
</div>

구조를 간략하게 설명하자면 각각의 입력 창들이 존재하고 필수로 입력을 받아야하는 곳에는 required를 설정해뒀다. '신청하기' 버튼을 누르면 모달이 뜨면서 정말로 신청하겠냐는 창이 뜨고 신청 버튼을 누르면 입력했던 값들이 서버로 전송이 되는 구조이다.

// 정규식 선언
let regTel = RegExp(/^[0-9]+$/);
let regContactTime = RegExp(/^[a-zA-Z0-9가-힣 ,.:~-]+$/);
let regJob = RegExp(/^[a-zA-Z가-힣 ]+$/);

// 변수 선언
let telCheck = true;
let contactTimeCheck = true;
let jobCheck = true;

$(document).ready(function (){

    //연락처 유효성 검사
    $("#phonenum").change(function () {
        if (!regTel.test($("#phonenum").val())) {
            $(".tel").text("숫자만 입력해주세요").css("color", "red");
            $("#phonenum").attr("class", "form-control is-invalid");
            telCheck = false;
        } else {
            $(".tel").text("");
            $("#phonenum").attr("class", "form-control is-valid");
            telCheck = true;
        }
    });
    
    ...
    
});

각각의 입력 필드에 불필요한 특수문자 등이 넘어오는걸 방지하기 위해 정규식을 선언해줬다. 전화번호, 연락가능 시간, 직업 각각 맞는 정규식 표현을 설정해두고 유효성 검사에서 통과하지 못했을 때 값이 넘어오는걸 방지하기 위해 telCheck라는 변수들도 설정해뒀다. 각각을 설명해보자면 regTel은 숫자만, regContactTime은 한글, 영어, 숫자, 몇몇 특수기호, 공백만 regJob은 영어, 한글, 공백만 허용한다.

입력을 했을 때 정규식과 일치하지 않으면 안내메세지와 함께 입력 폼에 빨간 줄이 생기게 되면 telCheck가 false 처리된다.

$("#adopt-btn").click(function () {

    if (!telCheck) {
        alert("입력형식을 확인해주세요.");
        $("#modal-default-2").modal('hide');
        $("#phonenum").focus();
        return false;
    }
    if (!contactTimeCheck) {
        alert("입력형식을 확인해주세요.");
        $("#modal-default-2").modal('hide');
        $("#contact_time").focus();
        return false;
    }
    if (!jobCheck) {
        alert("입력형식을 확인해주세요.");
        $("#modal-default-2").modal('hide');
        $("#job").focus();
        return false;
    }

    let pathUrl = "/fureverhomes/detail/{animal_id}/adopt".replace("{animal_id}", animalId);
    let breed = $('input[name="breeding"]:checked').val();
    breed = breed === "Y";

    let data = {
        animalId : animalId,
        phonenum: $("#phonenum").val(),
        contact_time: $("#contact_time").val(),
        residence: $("#residence").val(),
        job: $("#job").val(),
        breeding: breed,
        adopt_reason: $("#adopt_reason").val(),
        add_comment: $("#add_comment").val()
    };

    $.ajax({
        type: "POST",
        url: pathUrl,
        contentType: 'application/json',
        data: JSON.stringify(data),
        async: false,
        success: function (data, status) {
            alert("등록됐습니다. 마이페이지를 확인해주세요.")
            location.reload();
        },
        error: function (data, textStatus) {
            alert("이미 신청된 내역입니다. 마이페이지를 확인해주세요.")
            location.reload();
        }
    });
});

입양신청 버튼을 눌렀을 때 유효성 검사에서 false인 사항이 하나라도 있으면 모달창이 닫히면서 잘못 입력된 폼으로 포커스가 간다. breed의 경우 엔티티에서는 Boolean 타입을 가지므로 클라이언트에서 보낼 때부터 값이 Y일 때 true로 넘어오도록 설정해줬다.

 

 

 

 

2. 관심동물 등록

관심동물을 등록하는 기능과 입양을 신청하는 기능은 동일한 로직을 가지고 있다. 따라서 서비스나 컨트롤러 등 구체적인 로직은 설명하지 않고 이전에 생각했던 구현방법과 달라진 부분만 정리하도록 하겠다.

 

 

 

 

InterestEntity

 

 

관심동물 테이블은 회원, 동물 테이블의 다대다 관계를 다대일 관계로 바꿔주기 위한 중간 테이블의 역할을 한다. 회원 아이디와 동물 아이디를 외래키로 가지는 것 외에 다른 필드값을 가지고 있다면 새로운 엔티티로 만들어서 관리해주는게 훨씬 낫고 소규모 프로젝트가 아니라면 거의 대부분 그런 방법을 택하지만 나는 다른 필드값을 가지지 않기 때문에 @ManyToMany 를 이용해서 관리를 해주려고 계획했었다. 

그러나 이 기능을 구현하면서 여러 방법들을 찾아보다가 엔티티를 새로 가지도록 구성하는게 기능을 구현하는 데 있어서도 훨씬 편리하기도 하고, 유지보수 시에 있을 확장성을 생각했을 때도 맞는 방법이라고 생각해서 새로 만들게 되었다.

@Table(name = "interest_animal")
@Entity
@Getter
@NoArgsConstructor
public class Interest {

    @Id @Column(name = "INTEREST_ID")
    @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    @ManyToOne
    @JoinColumn(name = "ANIMAL_ID")
    private Animal animal;

    @Builder
    public Interest(Member member, Animal animal) {
        this.member = member;
        this.animal = animal;
    }

}

pk와 Member, Animal을 다대일 관계로 매핑해주고 있는 모습이다. 또한 Setter를 이용해서 직접적으로 엔티티를 수정하지 않기 위해 빌더를 이용했다.

//Member 엔티티
@OneToMany(mappedBy = "member")
    private Set<Interest> interests = new HashSet<>();
    
//Animal 엔티티
@OneToMany(mappedBy = "animal")
   private Set<Interest> interests = new HashSet<>();

또한 회원과 동물 엔티티에 Set 타입의 interests를 생성하여 양방향 매핑을 해주었다.

 

그리고 굳이 DTO를 생성하지 않고 엔티티에 생성해뒀던 빌더를 이용해서 바로 값을 넣어줬다.

interestRepository.save(new Interest(member, animal));

이외에 다른 기능들은 입양 기능과 똑같으므로 구체적인 설명을 생략하겠다.

 

 

 

 

결과

 

 

  • 관심 동물 등록

중복 데이터도 없고 정상적으로 등록이 될 경우 데이터베이스에 잘 들어오는 모습을 확인할 수 있다.

  • 관심 동물 중복 등록

같은 동물을 한번더 등록하려고 할때 alert 창이 뜨면서 안내글이 나타나는 모습이다.

  • 입양 신청서 작성 (유효성 검사 불합격 시)

입양 신청 폼에서 이렇게 정규식과 무관한 입력을 했을 시에 안내글과 폼이 빨갛게 바뀌고, 정상적으로 입력을 했을 시에는 초록색과 체크 폼으로 바뀌는 모습이다. 이런 상황에서 안내사항을 무시하고 입양 신청을 하게 되면

안내메세지와 함께 모달이 닫히면서 신청이 되지 않는다.

  • 입양 신청서 정상적으로 등록

다시 정상적으로 입력을 했을시엔 등록 성공 메세지가 뜨고, 데이터베이스를 확인해보면 값이 잘 들어와있는걸 볼 수 있다.

728x90