[Spring Boot] 10. 홈페이지 만들기 ① 회원 가입

shr·2022년 2월 21일
1

Spring

목록 보기
9/23
post-thumbnail
  1. 설계

    주소method방식리턴
    /member/joinGETMVCvoid
    POSTMVC체크코드 확인 페이지
    /member/id_checkGETRESTBoolean (ResponseEntity 사용)
    /member/check_joinGETMVCvoid
    POSTMVC로그인 페이지
    /member/loginGETMVCvoid
    POSTMVC루트 페이지 (스프링 시큐리티가 이동시켜 준다.)

    a. 아이디 사용 여부 확인
    b. 회원 가입
    c. 체크코드 확인
    d. 확인 후 로그인 화면으로 이동

    회원 가입 후 가입 확인 메일로 체크코드를 받는 c번 과정까지 해 본다.


  1. 기초 설정 잡기

    📝 해야 할 일!

    • SQL - member 테이블 생성
    • pom.xml 의존성 주입 (commons-lang3, log4jdbc-log4j2-jdbc4.1)
    • application.properties 수정
    • log4jdbc.log4j2.properties 추가
    • logback-spring.xml 추가
    • SecurityConfig 추가

    앞서 잡아 주었던 설정들과 동일하므로 코드는 생략한다.

  2. src/main/java - com.example.demo.entity - Member, Level 클래스 생성

    package com.example.demo.entity;
    
    import java.time.LocalDate;
    
    import com.example.demo.entity.*;
    
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.experimental.Accessors;
    
    @Data
    @AllArgsConstructor
    @Builder
    @Accessors(chain=true)
    public class Member {
        private String username;
        private String password;
        private String irum;
        private String email;
        private LocalDate birthday;
        private LocalDate joinday;
        private Boolean enabled;
        private String authority;
        private String checkcode;
        private Integer count;
        private Level levels;
    }
    package com.example.demo.entity;
    
    public enum Level {
        BRONZE, SILVER, GOLD;
    }

    💡 Level은 enum으로 생성한다.


  1. src/main/java - com.example.demo.controller - MemberControlloer 생성

    package com.example.demo.controller;
    
    import java.time.LocalDate;
    
    import javax.validation.Valid;
    import javax.validation.constraints.NotNull;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.*;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.WebDataBinder;
    import org.springframework.web.bind.annotation.*;
    import org.springframework.web.servlet.ModelAndView;
    
    import com.example.demo.controller.editor.DatePropertyEditor;
    import com.example.demo.entity.Member;
    import com.example.demo.service.MemberService;
    
    @Controller
    public class MemberController {
        @InitBinder
        public void init(WebDataBinder wdb) {	
            wdb.registerCustomEditor(LocalDate.class, new DatePropertyEditor());
        }
    
        @Autowired
        private MemberService service;
    
        // 1. 화면을 보여 주기만 하는 (void) 메소드들 먼저 만들기
        @GetMapping("/member/join")
        public void join() {
        }
    
        @GetMapping("/member/check_join")
        public void checkJoin() {
        }
    
        @GetMapping("/member/login")
        public void login() {
        }
    
        // 2. id_check 만들기, ResponseEntity 사용
        @GetMapping("/member/id_check")
        public ResponseEntity<Boolean> idCheck(@NotNull String username) {
            if (service.idCheck(username)==false)
                return ResponseEntity.status(HttpStatus.CONFLICT).body(false);
            return ResponseEntity.status(HttpStatus.OK).body(true);
            // return ResponseEntity.ok(true); -> 바로 윗 줄은 이렇게도 작성 가능하다.
        }
    
        // @Valid를 사용하면 커맨드 객체(사용자가 입력한 값을 담은 클래스)에 대한 검증을 스프링이 수행한다.
        // 객체가 아닌 파라미터에 대해 검증하려면 @Validated 어노테이션을 컨트롤러에 지정해야 한다.
        @PostMapping("/member/join")
        public ModelAndView join(@Valid Member member) {
            service.join(member);
            return new ModelAndView("/member/check_join");
        }
    
        @PostMapping("/member/check_join")
        public String checkJoin(String checkcode) {
            service.joinCheck(checkcode);
            return "member/login";
        }
    }

    📝 아이디 사용 여부를 확인했을 때 어떤 경우가 success일까? 웹에서의 성공(200)은 서버에서 오류가 발생하지 않았다는 뜻이다. 아이디가 사용 가능하든 불가능하든(true, false) 모두 서버 측에서 보면 200이기 때문에, ajax의 success가 동작한다. 그렇다면 true, false에 따른 if문을 걸어 주어야 하는데 if문은 위험해 가급적 안 쓰는 것이 좋다. 요즘은 내가 원하는 결과면 200, 원하지 않는 결과면 다른 상태 코드(ex. 409)가 나오게끔 코드를 짜는 추세이다.


  1. src/main/java - com.example.demo.service - MemberService 생성

    package com.example.demo.service;
    
    import java.time.LocalDate;
    
    import javax.mail.MessagingException;
    import javax.mail.internet.MimeMessage;
    
    import org.apache.commons.lang3.RandomStringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.mail.javamail.*;
    import org.springframework.security.crypto.password.*;
    import org.springframework.stereotype.Service;
    
    import com.example.demo.dao.MemberDao;
    import com.example.demo.entity.Level;
    import com.example.demo.entity.Member;
    
    @Service
    public class MemberService {
        @Autowired
        private MemberDao dao;
        @Autowired
        private PasswordEncoder passwordEncoder;
        @Autowired
        private JavaMailSender javaMailSender;
    
        // 메일 보내기 (Java Mail Sender 이용)
        private void sendMail(String from, String to, String subject, String text) {
            MimeMessage message = javaMailSender.createMimeMessage();
            MimeMessageHelper helper;
            try {
                helper = new MimeMessageHelper(message, false, "utf-8");
                helper.setFrom(from);
                helper.setTo(to);
                helper.setSubject(subject);
                helper.setText(text, true);
                javaMailSender.send(message);
            } catch (MessagingException e) {
                e.printStackTrace();
            }	
        }
    
        public boolean idCheck(String username) {
            return !dao.existsById(username);
        }
    
        // 비밀번호 암호화 (PasswordEncoder 이용 - 시큐리티 설정 파일에 Bean 추가해 주기)
        public void join(Member member) {
            String checkcode = RandomStringUtils.randomAlphanumeric(20);
            String encodedPassword = passwordEncoder.encode(member.getPassword());
    
            member.setJoinday(LocalDate.now()).setAuthority("ROLE_USER").setCheckcode(checkcode)
                .setEnabled(false).setLevels(Level.BRONZE).setPassword(encodedPassword).setCount(0);
    
            dao.save(member);
    
            // 문자열 변수 StringBuilder 사용
            String text = new StringBuilder("<h1>가입 확인 코드</h1>")
                    .append("<p>가입을 마무리하려면 아래 코드를 화면에 입력해 주세요 (*˙˘˙)♡</p>")
                    .append(checkcode).toString();
            sendMail("admin@icia.com", member.getEmail(), "가입 확인 메일 (。・ө・。)", text);
        }
    
        public void joinCheck(String checkcode) {		
        }
    }

    PasswordEncoder를 이용해 암호화를 해 주기 위해 com.example.demo에 자동 생성된 시큐리티 설정 파일에 빈을 생성하고, 등록해 준다.

        // @Bean : 스프링 ApplicationContext에 객체를 등록 (DI 대상으로 지정)
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }

  1. src/main/java - com.example.demo.dao - MemberDao 생성

    package com.example.demo.dao;
    
    import org.apache.ibatis.annotations.Mapper;
    
    import com.example.demo.entity.Member;
    
    @Mapper
    public interface MemberDao {
        // 인터페이스 내부의 추상 메소드는 무조건 public! (생략 가능)
        // MyBatis에서 결과가 boolean으로 나가려면, 결과가 1<n 또는 0이어야 한다.
        public Boolean existsById(String username);
        public Integer save(Member member);
    }

    💡 @mapper 걸어 주는 것 잊지 말자!


  1. src/main/resources - mapper - memberMapper.xml 생성

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.example.demo.dao.MemberDao">
        <select id="existsById" resultType="boolean">
            select count(*) from member where username=#{username} and rownum=1
        </select>
    
        <insert id="save">
            insert into member(username, password, irum, email, birthday, levels) 
            values(#{username}, #{password}, #{irum}, #{email}, #{birthday}, #{levels})
        </insert>
    </mapper>

    💡 namespace 경로 제대로 걸려 있는지 확인하자!


  1. src/main/java - com.example.demo.controller.editor - DatePropertyEditor 생성

    package com.example.demo.controller.editor;
    
    import java.beans.PropertyEditorSupport;
    import java.time.LocalDate;
    
    public class DatePropertyEditor extends PropertyEditorSupport {
        @Override
        public void setAsText(String text) throws IllegalArgumentException {
            // text 파라미터 : 사용자가 입력한 문자열, 지금 같은 경우 "2020-11-20"
            String str[] = text.split("-");		// js : ["2020", "11", "20"]
            Integer year = Integer.parseInt(str[0]);
            Integer month = Integer.parseInt(str[1]);
            Integer day = Integer.parseInt(str[2]);
    
            LocalDate date = LocalDate.of(year, month, day);
    
            setValue(date);
        }
    }

    "2022-02-21"로 들어오는 날짜 입력을 LocalDate로 사용하기 위해 PropertyEditor 인터페이스의 SetAsText를 사용해 편집해 준다.

    📝 PropertyEditor를 이용하기 위해서는 controller에 @initBinder를 추가해 주어야 한다.

        @InitBinder
        public void init(WebDataBinder wdb) {	
            wdb.registerCustomEditor(LocalDate.class, new DatePropertyEditor());
        }

  1. src/main/resources - templates - fragments - header.html, nav.html, aside.html,footer.html 생성

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
    <body>
        <div id="header" class="col s12">
            <p>Web Board</p>
        </div>
    </body>
    </html>
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
    <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
    </head>
    <body>
    <div th:fragment="nav" id="nav" class="navbar navbar-inverse">
        <div class="container-fluid">
            <div class="navbar-header">
                <a class="navbar-brand" href="/">HOME</a>
            </div>
            <ul sec:authorize="isAnonymous()" class="nav navbar-nav" id="menu_anonymous">
                <li><a href="/member/join">회원가입</a></li>
                <li><a href="/member/find_id">아이디 찾기</a></li>
                <li><a href="/member/reset_password">비밀번호 찾기</a></li>
                <li><a href="/member/login">로그인</a></li>
                <li><a href="/board/list">게시판</a></li>
            </ul>
            <ul sec:authorize="isAuthenticated()" class="nav navbar-nav" id="menu_authenticated">
                <li><a href="/board/list">게시판으로</a></li>
                <li><a href="/board/write">글쓰기</a></li>
                <li><a href="#" id="logout">로그아웃</a></li>
            </ul>
        </div>
        <!-- 타임리프 html에 js 사용하는 방식 -->
        <script th:inline="javascript">
            // 동적으로 만들어진 버튼의 이벤트 처리를 하려면 선택자를 2개 주어야 한다. -> $(넓은 선택자).on("이벤트 이름", "실제 선택자", function())
            $("#menu_authenticated").on("click", "#logout", (e)=> {
                /*
                    <div id="outer" onclick='alert("outer")'>
                        <div id="innter" onclick='alert("inner")'>여기 클릭</div>
                    </div>
    
                    글자를 클릭하면 js가 봤을 때는 #inner를 클릭한 것이면서, #outer를 클릭한 것이기도 하다.
                    경고창은 두 개가 뜨게 되는데, 동작 순서는 inner -> outer이다. (좁은 쪽에서 넓은 쪽으로 전달된다.)
                    전달되는 것을 끊으려면 e.stopPropagation
    
                    동작이 2 동작인 이벤트 (ex. a 태그와 submit - 클릭하고 나면 이동한다.)
                    이때 두 번째 동작을 비활성화, 즉 이동 금지를 하려면 e.preventDefault
                */
                e.preventDefault();
                var choice = confirm('로그아웃하시겠습니까?');
                if(choice==false)
                    return;
                $.ajax({
                    url:"http://localhost:8081/member/logout",
                    method: "post",
                }).done(()=> location.href = '/');
    
            });	
        </script>
    </div>
    </body>
    </html>

    💡 e.stopPropagation과 e.preventDefault

    <div id="outer" onclick='alert("outer")'>
        <div id="innter" onclick='alert("inner")'>여기 클릭</div>
    </div>

    글자를 클릭하면 js가 봤을 때는 #inner를 클릭한 것이면서, #outer를 클릭한 것이기도 하다. 경고창은 #inner와 #outer 모두 뜨게 되는데, 동작 순서는 #inner -> #outer으로, 좁은 쪽에서 넓은 쪽으로 전달된다. 이때 전달되는 것을 끊으려면 e.stopPropagation을 쓴다.

    동작이 2 동작인 이벤트, 예를 들어 a 태그와 submit (클릭하고 나면 이동)와 같은 것이 있을 때, 두 번째 동작을 비활성화(이동 금지)하려면 e.preventDefault를 쓴다.

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
    </head>
    <body>
        <div id="aside">
            <div id="msg">광고</div>
            <div>광고</div>
            <div>광고</div>
        </div>
    </body>
    
    </body>
    </html>
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
    </head>
    <body>
        <div style="border: 1px solid gray; text-align: center;">
            <h5 class="white-text">copyright &copy; 2022 Web</h5>
        </div>
    </body>
    </html>

  1. src/main/resources - templates - member - join.html 생성

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
        <title>회원 가입</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
        <link rel="stylesheet" href="/css/main.css">
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
        <script src="/script/check.js"></script>
        <!-- sweetalert2 -->
        <script src="//cdn.jsdelivr.net/npm/sweetalert2@11"></script>
    </head>
        <script>
            // 1. 오류 메시지 출력 함수 : 아이디부터 생일까지 6개의 input에 대한 공통 오류 메시지 처리
            // check.js로 뺐다.
    
            // 2. 아이디 확인 (재사용을 위해서 함수로 만드는 것)
            // check.js로 뺐다.
    
            // 3. check.js의 함수들을 이용해 입력 값을 검증, 가입 버튼 누르면 서버로 전송 
            $(document).ready(function() {
                // 1. 아이디 입력 오류 처리 -> usernameCheck 후 성공하면 ajax
                $('#username').focusout(function() {
                    // 아이디를 입력하지 않았거나, 패턴을 통과하지 못했다면 작업 중단
                    if (usernameCheck() == false)
                        return false;
                    // 서버로 ajax 전송, GET 방식일 때는 파라미터를 포함한 주소만 적어도 된다.
                    $.ajax("/member/id_check?username=" + $("#username").val())
                    .done(()=>$("#username_msg").text("좋은 아이디네요!").attr("class", "success"))
                    .fail(()=>$("#username_msg").text("사용 중인 아이디입니다.").attr("class", "fail"));
                });
    
                // 4. 비밀번호, 이름, 이메일, 생일은 입력이 잘못된 경우 오류 메시지만 출력하면 된다.
                // passwordCheck() 이렇게 쓰지 말 것! 그럼 페이지 연 순간 함수 바로 시작됨!
                $('#password').focusout(passwordCheck);
                $('#password2').focusout(password2Check);
                $('#irum').focusout(irumCheck);
                $('#email').focusout(emailCheck);
                $('#birthday').focusout(birthdayCheck);
    
                // 5. 회원 가입 버튼을 클릭하면 usernameCheck()부터 birthdayCheck()까지 실행한다.
                $('#join').click(function() {
                    // &&, || 연산은 결과(참거짓)가 판별되면 연산을 중단한다. usenameCheck에서 실패한 경우 if문은 실행을 바로 중단한다.
                    if ((usernameCheck() && passwordCheck() && password2Check() && irumCheck() && emailCheck() && birthdayCheck()) == false)
                        return false;
    
                    // 아이디 사용 가능한지 확인한 다음 사용 가능하면 회원 가입 처리
                    $.ajax("/member/id_check?username=" + $('#username').val())
                        .done(() => {
                            $('#join_form').submit();
                        })
                        .fail(() => $('#username_msg').text("사용 중인 아이디입니다.").attr('class', fail));
                })
            })
        </script>
    <body>
    <div id="page">
        <header th:replace="/fragments/header.html">
        </header>
        <nav th:replace="/fragments/nav.html">
        </nav>
        <div id="main">
            <aside th:replace="/fragments/aside.html">
            </aside>
            <section>
                <form id="join_form" method="post" action="/member/join">
                    <div>
                        <label for="username">아이디</label>
                        <span id="username_msg"></span>
                        <div class="form-group">
                            <input type="text" class="form-control" id="username" name="username">
                        </div>
                    </div>
                    <div>
                        <label for="irum">이름</label>
                        <span id="irum_msg"></span>
                        <div class="form-group">
                            <input type="text" class="form-control" id="irum" name="irum">
                        </div>
                    </div>
                    <div>
                        <label for="password">비밀번호</label>
                        <span id="password_msg"></span>
                        <div class="form-group">
                            <input id="password" type="password" class="form-control" name="password">
                        </div>
                    </div>
                    <div>
                        <label for="password2">비밀번호 확인</label>
                        <span id="password2_msg"></span>
                        <div class="form-group">
                            <input id="password2" type="password" class="form-control">
                        </div>	
                    </div>
                    <div>
                        <label for="email">이메일</label>
                        <span id="email_msg"></span>
                        <div class="form-group">
                            <input id="email" type="text" name="email" class="form-control">
                        </div>
                    </div>
                    <div>
                        <label for="birthday">생년월일</label>
                        <span id="birthday_msg"></span>
                        <div class="form-group">
                            <input id="birthday" type="date" name="birthday" class="form-control">
                        </div>
                    </div>
                    <div class="form-group" style="text-align: center;">
                        <button type="button" id="join" class="btn btn-info">가입</button>&nbsp;&nbsp;&nbsp;&nbsp;
                        <a id="home" class="btn btn-primary" href="/">HOME</a>
                    </div>
                </form>
            </section>
        </div>
        <footer th:replace="/fragments/footer.html">
        </footer>
    </div>
    </body>
    </html>

    📝 ajax가 성공했을 때 어떤 작업을 수행하려고 한다면,그 작업 내용은 반드시 $.ajax 내부에 있어야 한다.


  1. src/main/resources - static - script - check.js 생성

    /*
        항목				패턴
        아이디				대문자, 숫자 8~10 자
        비밀번호			특수문자를 포함하는 영, 숫자 8~10 자
        비밀번호 확인		비밀번호와 같다.
        이름				한글 2~10 자
        이메일				/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i; ★ 이메일 규칙은 그냥 복붙하세용!
                            /i (case insensitive, 대소문자 구분하지 말라는 것)
                            /g (global, 정규식은 패턴을 찾으면 중지하는데, 중지하지 말고 모두 찾으라는 것)
                                js에서 test() 말고 replace()할 때 필요하다. 
        생일				숫자 4자 - 2자 - 2자
    */
    
    // 1. 패턴 정의
    const usernamePattern = /^[0-9A-Z]{8,10}$/;
    const irumPattern = /^[가-힣]{2,10}$/;
    // ()는 독립된 조건, ?=는 앞부터 찾으라는 것(전방 탐색)
    // .은 임의의 글자가 * 0글자 이상 -> 특수문자가 1 글자 이상
    const passwordPattern = /^(?=.*[!@#$%^&*])^[A-Za-z0-9!@#$%^&*]{8,10}$/;
    const emailPattern = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i;
    const birthdayPattern = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
    
    // 2. 오류 메시지 출력 함수 : 아이디부터 생일까지 6개의 input에 대한 공통 오류 메시지 처리
    // function printCheckMessage(입력 값, 패턴, 출력할 메시지, 출력할 span)
    function check(value, pattern, message, span) {
        // 입력 안 함
        if (value == "") {
            span.text("필수 입력입니다.").attr('class', 'fail');
            return false;
        }
        // 패턴 체크
        if (pattern.test(value)==false) {
            span.text(message).attr('class','fail');
            console.log(value);
            console.log(pattern.test(value));
            return false;
        }
        return true;
    }
    
    // 3-1. 입력한 아이디 확인 함수
    function usernameCheck() {
        // 입력한 아이디를 대문자로 변경
        const $username = $("#username").val().toUpperCase();
        $("#username").val($username);
        return check($username, usernamePattern, "아이디는 대문자와 숫자 8~10자입니다", $("#username_msg"));
    }
    
    // 3-2. 입력한 이름 확인 함수
    function irumCheck() {
        $("#irum_msg").text("");
        return check($("#irum").val(), irumPattern, "이름은 한글 2~10자입니다", $("#irum_msg"));
    }
    
    // 3-3. 입력한 비밀번호 확인 함수
    function passwordCheck() {
        $("#password_msg").text("");
        return check($("#password").val(), passwordPattern, "비밀번호는 영숫자와 특수문자 8~10자입니다", $("#password_msg"));	
    }
    
    // 3-4. 입력한 비밀번호 확인이 비밀번호와 일치하는지 확인 함수 - 패턴이 없으므로 check() 함수를 사용하지 않는다.
    function password2Check() {
        $("#password2_msg").text("");
        const $password2 = $("#password2").val();
        if($password2=="") {
            $("#password2_msg").text("필수입력입니다").attr("class","fail");
            return false;
        } 
        if($password2!==$("#password").val()) {
            $("#password2_msg").text("비밀번호가 일치하지 않습니다").attr("class","fail");
            return false;
        }
        return true;
    }
    
    // 3-5. 입력한 이메일 확인 함수
    function emailCheck() {
        $("#email_msg").text("");
        return check($("#email").val(), emailPattern, "정확한 이메일을 입력하세요", $("#email_msg"))
    }
    
    // 3-6. 입력한 생일 확인 함수
    function birthdayCheck() {
        $("#birthday_msg").text("");
        return check($("#birthday").val(), birthdayPattern, "정확한 생일을 입력하세요", $("#birthday_msg"))
    }

  1. src/main/resources - static - css - main.css 생성

    @charset "UTF-8";
    |* {
        margin: 0;
        padding: 0;	
    }
    
    ul, li {
        list-style: none;
    }
    
    /* 웹 폰트 : cdn으로 폰트 지정 */
    @import url(http://fonts.googleapis.com/earlyaccess/nanumgothic.css);
    header, nav, aside, footer, section {
        font-family : "Nanum Gothic", sans-serif;
    }
    
    /* 전체적인 레이아웃 설정 */
    #page {
        width: 1005px;
        margin: 0 auto;
    }
    
    #header  {
        border : 1px solid #d89a1d;
        letter-spacing: 0.75em
    }
    #header p {
        font-size: 1.75em;
        font-weight: bold;
        text-align: center;
    }
    
    #aside {
        width: 200px;
        height: 303px;
        float: left;
        background-color: #ccc;
    }
    
    #aside div {
        width: 150px;
        background-color: #eee;
        border: 1px solid #ddd;
        height: 100px;
        line-height: 100px;
        text-align: center;
        margin: 0 auto;
    }
    
    section {
        width: 800px;
        padding: 20px;
        float: right;
        border: 1px solid orange;
        min-height: 600px;
    }
    
    #main {
        overflow: hidden;
    }
    
    .custom {
        width: 100px;
    }
    
    /* 입력값 체크 성공, 실패 메시지에 대한 클래스*/
    .success { 
        color: blue; 
        font-size: 0.75em; 
    }
    .fail { 
        color: red; 
        font-size: 0.75em; 
    }
profile
못하다 보면 잘하게 되는 거야 ・ᴗ・̥̥̥

0개의 댓글