118일차

1. 회차관리하기

회차처리하면 바꿔야할것들이 많다
목록들이 바뀌어야한다.

유효성검사를 지금까지 프론트단에서만 했었는데 인터넷강의에서 valitation을 배워서 백에서 처리하는 방법을 이용해보고자 한다.
프론트에서만 한다면 악의적으로 콘솔로그에서 값을 변경해서 보낼 수도 있다. 물론 그냥 사람은 할순 없겟지만 노리고자 한다면 가능하다.

즉 악의적으로 콘솔로그에서 변경하는것, 막지 못하는 설정들 검증이 필요하다.

이전까지 했던 것들은 검증, 오류처리 등등 많이 미흡했다.

1.1 라이브러리

검증을 하기 위해서는 검증 라이브러리가 필요하다.

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>2.5.2</version>
</dependency>

유효성 검사에 사용할 수 있는 것은 다음과 같다.

@Null  // null만 혀용
@NotNull  // null을 허용하지 않는다.
@NotEmpty  // null, ""을 허용하지 않는다.
@NotBlank  // null, "", " " 모두 허용하지 않는다.

@Email  // 이메일 형식을 검사. 다만 ""의 경우를 통과 시킨다.
@Pattern(regexp = )  // 정규식을 검사할 때 사용
@Size(min=, max=)  // 길이를 제한할 때 사용

@Max(value = )  // value 이하의 값을 받을 때 사용
@Min(value = )  // value 이상의 값을 받을 때 사용

@Positive  // 값을 양수로 제한
@PositiveOrZero  // 값을 양수와 0만 가능하도록 제한

@Negative  // 값을 음수로 제한합니다.
@NegativeOrZero  // 값을 음수와 0만 가능하도록 제한

@Future  // 현재보다 미래
@Past  // 현재보다 과거

@AssertFalse  // false 여부, null은 체크하지 않는다.
@AssertTrue  // true 여부, null은 체크하지 않는다.

1.2 도메인

위의 어노테이션을 기반으로 값을 넣어준다. message는 검증 실패시 돌려줄 값이다.

@Getter
@Setter
@ToString
public class ExhibitionInfo {

    @NotNull(message = "공백은 허용되지 않습니다.")
    private Integer round;

    @NotBlank(message = "공백은 허용되지 않습니다.")
    private String location;

    @NotBlank(message = "공백은 허용되지 않습니다.")
    @Pattern(regexp = "\\w+@\\w+\\.\\w+", message = "이메일 형식이 올바르지 않습니다.")
    private String managerEmail;

    @NotBlank(message = "공백은 허용되지 않습니다.")
    private String organizer;

    @NotBlank(message = "공백은 허용되지 않습니다.")
    private String startDate;

    @NotBlank(message = "공백은 허용되지 않습니다.")
    private String endDate;

    private String etc;

    private String bus;

    private String subway;

    private List<String> fileName;
}

1.3 view

<div class="container-lg mt-3">
    <div class="row justify-content-center">
        <div class="col-12 col-md-8 col-lg-6">
            <h3>회차 등록 하기</h3>
            <form:form method="post" modelAttribute="exhibitionInfo" enctype="multipart/form-data">
                <div class="mb-3">
                    <label for="round" class="form-label">회차</label>
                    <form:input path="round" type="number" id="round" class="form-control"/>
                    <form:errors path="round" cssClass="field-error"/>
                </div>

                <div class="mb-3">
                    <label for="email" class="form-label">담당자 이메일</label>
                    <form:input path="managerEmail" id="email" type="email" class="form-control"/>
                    <form:errors path="managerEmail" cssClass="field-error"/>
                </div>

                <div class="mb-3">
                    <label for="organizer" class="form-label">개최자</label>
                    <form:input path="organizer" type="text" class="form-control"/>
                    <form:errors path="organizer" cssClass="field-error"/>
                </div>

                <div class="mb-3">
                    <label for="start-date" class="form-label">개최일</label>
                    <form:input path="startDate" id="start-date" type="date" class="form-control"/>
                    <form:errors path="startDate" cssClass="field-error"/>
                </div>

                <div class="mb-3">
                    <label for="end-date" class="form-label">폐막일</label>
                    <form:input path="endDate" id="end-date" type="date" class="form-control"/>
                    <form:errors path="endDate" cssClass="field-error"/>
                </div>

                <div class="mb-3">
                    <label for="input-address" class="form-label">주소</label>
                    <div class="input-group">
                        <form:input path="location" id="input-address" type="text" class="form-control mb-1"
                                    readonly="true" placeholder="도로명 주소"/>
                        <button class="btn btn-outline-secondary" type="button" id="search-address-btn">주소검색</button>
                    </div>
                    <form:errors path="location" cssClass="field-error"/>
                </div>

                <div class="mb-3">
                    <label for="bus" class="form-label">버스</label>
                    <form:input path="bus" id="bus" type="text" class="form-control"/>
                    <form:errors path="bus" cssClass="field-error"/>
                    <div class="form-text">, 없이 띄어쓰기로 구분해서 넣어주세요</div>
                </div>

                <div class="mb-3">
                    <label for="subway" class="form-label">지하철</label>
                    <form:input path="subway" id="subway" type="text" class="form-control"/>
                    <form:errors path="subway" cssClass="field-error"/>
                    <div class="form-text">, 없이 띄어쓰기로 구분해서 넣어주세요</div>
                </div>

                <div class="mb-3">
                    <label for="etc" class="form-label">기타</label>
                    <form:input path="etc" type="text" class="form-control"/>
                    <form:errors path="etc" cssClass="field-error"/>
                </div>

                <div class="mb-3">
                    <label for="form-file" class="form-label">첨부 파일</label>
                    <input class="form-control" name="files" type="file" id="form-file" multiple>
                    <div class="form-text">총 10MB, 하나의 파일을 1MB를 초과할 수 없습니다.</div>
                    <div class="form-text">로고는 logo.png로 등록해주세요</div>
                </div>

                <div>
                    <button class="btn btn-primary" type="submit">등록</button>
                    <a href="/admin/round" class="btn btn-primary">현재 회차 정보 보기</a>
                </div>
            </form:form>
        </div>
    </div>
</div>

jsp에서 javabean validation을 사용하려면 form:form태그를 사용해야한다.
<form:form>은 Spring Framework에서 제공하는 태그 라이브러리인 Spring Form Tag Library의 하나이다.
이 태그 라이브러리를 사용하면 HTML 폼을 생성하고, 폼 데이터를 쉽게 바인딩하고 유효성 검사 등의 기능을 지원한다.

Spring Form Tag Library는 JSP(JavaServer Pages) 페이지에서 사용되며, 폼 요소와 폼 데이터를 관리할 때 도움이 되는 다양한 커스텀 태그를 제공한다.
<form:form> 태그는 폼 요소를 생성하는 태그로, <form> 태그와 유사하지만 Spring Form Tag Library의 특수한 기능을 사용할 수 있다.

다음은 예시이다.

<form:form method="HTTP_METHOD" action="FORM_ACTION_URL" modelAttribute="FORM_MODEL_ATTRIBUTE" enctype="ENCTYPE">
    <!-- 폼 요소들과 버튼 등이 들어가는 부분 -->
</form:form>

기존의 form과 사용방법이 유사하다.

method: HTTP 메서드를 지정한다. 주로 "post" 또는 "get" 값을 사용한다.
action: 폼이 제출될 때 처리할 URL을 지정한다.
modelAttribute: 폼 데이터를 바인딩할 객체를 지정한다.
컨트롤러와 연결되는 폼 데이터 객체이다.
enctype: 폼 데이터를 서버로 전송할 때 사용되는 인코딩 타입을 지정한다.
주로 "application/x-www-form-urlencoded" 또는 "multipart/form-data"를 사용한다
파일 업로드가 필요한 경우 "multipart/form-data"를 사용한다
form:form 태그 내부에는 HTML 폼 요소들이 포함되며, 각 요소들은 Spring Form Tag Library의 커스텀 태그를 사용하여 데이터 바인딩, 유효성 검사 등의 기능을 활용할 수 있다.
예를 들어, form:input, form:select, form:checkbox, form:radiobutton, form:textarea 등의 커스텀 태그를 사용할 수 있다.

1.4 컨트롤러

@GetMapping("/reg")
public String reg(Model model) {
    model.addAttribute("exhibitionInfo", new ExhibitionInfo());
    return "admin/round/reg";
}

위의 form:form태그를 사용하기 위해서 담을 수 있는 객체가 있어야하기 때문에 모델에 새객채를 만들어서 보내주어야한다.
그래야 모델에 담아서 값을 보낸 후 검증을 할 수 있게 되는것이다.
이것이 기존 form태그와의 차이점이라고 볼 수 있다.
실제 검증 코드는 다음과 같다.

검증받는 객체에 @Valid 어노테이션을 붙이면 domain에 넣엇던 어노테이션에 따라서 검사를 하게 된다.
그 검사결과를 BindingResult객체에 담게 된다.
검증 실패시 bindingResult객체에 에러 정보가 있으므로 에러정보가 잇다면 그 페이지로 돌아가게 된다.

그 밑부분은 또다른 검증 실패인데 특정 조건을 사용해서 검증을 하고 싶을때 따로 추가할 수 있다.
나의 경우 등록하려는 회차보다 현재 회차가 낮으면 안되기 때문에 이런 조건 처리를 해주었다.
bindingResult에 rejectValue메소드를 추가해준다. 필드명, 오류코드, 메시지를 담아준다.

 @PostMapping("/reg")
public String regProc(@Valid ExhibitionInfo exhibitionInfo,
                      BindingResult bindingResult,
                      @RequestParam(value = "files", required = false) MultipartFile[] files,
                      RedirectAttributes rttr) {
    Integer currentRound = exhibitionInfoService.getCurrentRound();

    Integer regRound = exhibitionInfo.getRound();

    // 검증에 실패 시 bindingResult 객체에 에러 정보 담음
    if (bindingResult.hasErrors()) {
        return "admin/round/reg";
    }

    if (regRound != null) {
        if (regRound <= currentRound) {
            bindingResult.rejectValue("round", "round.invalid", "이전회차보다 같거나 낮게 설정할 수 없습니다.");
        }
    }

    // 검증에 성공한 경우 처리 로직 실행
    // 여기 까지 오면 exhibitionInfo 객체는 검증이 통과된 상태이다.
    Boolean ok = false;

    try {
        // 등록하기
        ok = exhibitionInfoService.reg(exhibitionInfo, files);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }

    // 채용담당자 권한을 company로 전부 변경하기

    // 공고 전체 마감

    // 필요한 작업을 수행한 후 다른 페이지로 리다이렉트
    if (ok) {
        rttr.addFlashAttribute("message", "등록이 완료되었습니다.");
        return "redirect:/admin/round";
    } else {
        rttr.addFlashAttribute("message", "등록에 실패했습니다.");
        return "redirect:/admin/round/reg";
    }
}

만약 에러코드가 있다면 해당 페이지로 포워드 하게 되는데 하면서 오류코드를 담아서 가져가서 위에서 작성한 다음 코드에 값을 넣게 된다.

<form:errors path="etc" cssClass="field-error"/>

path는 필드명이고 해당 필드에 각각 에러값들을 넣어서 반환하게 된다. cssClass는 에러일때 디자인을 의미한다.

2023.07.30

버그를 하나 발견했다. S3에 직접가서 등록한 것은 해당 링크로 가면 다운이 안되고 새 페이지가 열리게된다.
자바코드로 업로드한것만 똑같이 링크에 요청하면 다운로드가 된다.
권한 설정차이가 있는 것으로 보인다.

<%--
<form:form modelAttribute="company" method="post" action="/api/user/recruiter/"
enctype="multipart/form-data">



<form:input id="input-company-name" path="companyName" type="text" class="form-control"
name="companyName"
placeholder="기업이름을 입력해주세요."/>
<form:errors path="companyName" cssClass="error"/>

<div class="mb-3">
    <label for="input-registration-number" class="form-label">사업자등록번호 *</label>
    <div class="form-text">- 포함 입력</div>
    <form:input id="input-registration-number" path="registrationNumber" type="text" class="form-control"
                name="registrationNumber"
                placeholder="사업자등록번호를 입력해주세요"/>
</div>

<div class="mb-3">
    <label for="input-number-of-employees" class="form-label">사원수 *</label>
    <form:input id="input-number-of-employees" path="numberOfEmployees" type="number" class="form-control"
                name="numberOfEmployees"/>
    <form:errors path="numberOfEmployees" cssClass="error"/>
</div>

<div class="mb-3">
    <label for="input-establishment-date" class="form-label">설립일 *</label>
    <form:input id="input-establishment-date" path="establishmentDate" type="date" class="form-control"
                name="establishmentDate"/>
    <form:errors path="establishmentDate" cssClass="error"/>
</div>

<div class="mb-3">
    <label for="input-revenue" class="form-label">매출액 *</label>
    <form:input id="input-revenue" path="revenue" type="number" class="form-control" name="revenue"/>
    <form:errors path="revenue" cssClass="error"/>
</div>

<div class="mb-3">
    <label for="input-ceo-name" class="form-label">대표자명 *</label>
    <form:input id="input-ceo-name" path="ceoName" type="text" class="form-control" name="ceoName"/>
    <form:errors path="ceoName" cssClass="error"/>
</div>

<div class="mb-3">
    <label for="input-industry-id" class="form-label">업종 *</label>
    <form:select path="industryId" name="industryId" class="form-select" id="input-industry-id">
    </form:select>
</div>

<div class="mb-3">
    <label for="input-address" class="form-label">주소 *</label>
    <div class="col-sm-6 mb-1">
        <div class="input-group">
            <input type="text" id="post-code" name="postalCode" class="form-control input-sm"
                   placeholder="우편번호" readonly>
            <button class="btn btn-outline-secondary" name="address" type="button" id="search-address-btn">
                주소검색
            </button>
        </div>
    </div>
    <input id="input-address" type="text" class="form-control mb-1" readonly placeholder="도로명 주소"/>
    <form:input path="detailAddress" name="detailAddress" id="input-detail-address" type="text"
                class="form-control mb-1" placeholder="상세주소"/>
</div>

<div class="mb-3">
    <label for="form-file" class="form-label">첨부 파일 *</label>
    <input class="form-control" name="files" type="file" id="form-file" multiple>
    <div class="form-text">총 10MB, 하나의 파일을 1MB를 초과할 수 없습니다.</div>
</div>

<div>
    <div class="form-text">정보가 충분하지 않을 시 신청이 보류되거나 반려될 수 있습니다. <br>
        * 표시는 필수로 입력해야합니다.
    </div>
    <button id="submit-btn" type="submit" class="btn btn-primary">신청</button>
</div>
</div>

--%>

+ Recent posts