[Spring] Week03

[수업 목표]

  1. 페이지를 만들기 위한 HTML,CSS, Javascript를 익힌다.
  2. 스프링을 이용해 API를 만들고 기능 확인하는 법을 손에 익힌다.
  3. 타임라인 서비스를 완성한다.

[목차]

모든 토글을 열고 닫는 단축키 Windows : Ctrl + alt + t Mac : + + t


01. [3주차] 이번 주 배울 것

  • 만들 프로젝트 미리보기

    End to End 로 프로젝트를 완성해보는 3주차입니다! 🔥

    • [코드스니펫] 타임라인 서비스 주소

      http://spring.spartacodingclub.kr/timeline
      
  • 서버 완성하기 - Memo API
    • 2주차 때 배운 내용을 바탕으로 CRUD 기능을 가진 Memo API를 만듭니다.
    • 키워드: RestController, Service, Repository, RequestDto
  • HTML, CSS
    • 웹 화면을 구성하는 HTML, CSS에 대해 배워봅니다.
  • Javascript, jQuery
    • 웹 화면을 움직이게 만들어주는 Javascript의 기초를 배웁니다.
    • jQuery 를 이용하여 화면을 숨기고, 나타내는 방법을 익혀봅니다.
  • 클라이언트 완성하기
    • HTML, CSS, Javascript 를 바탕으로 클라이언트를 완성하고, 서버와 연결합니다.
    • 완성된 페이지를 로컬에서 확인합니다.

02. [3주차] 프로젝트 만들고 API 설계하기

  • 잠깐, 전체 그림을 다시 볼까요?

    https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0f40eb9b-9907-4600-9bdc-3437c36cff54/Untitled.png

    • Controller - Service - Repository 3계층이 존재한다는 것, 기억하시죠?
    • 우리는 안에서 바깥으로, 즉 Repository 쪽부터 Service, Controller 방향으로 만들어나갈거에요.
    • 이번 시간에는 Repository 부터 만들어보겠습니다.
  • 프로젝트 새로 만들기

    팁) 이미지를 더블클릭하시면 크게 보실 수 있어요 😎

    1. 인텔리제이를 실행합니다.
    2. New Project를 클릭합니다.

      https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8d48ec97-b7f7-456f-ba71-0e10f5c5f3f7/Untitled.png

    3. 왼쪽 메뉴에서 “Spring Initializr”를 클릭하고 “Next”를 클릭합니다.

      https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dd6c3ad3-eb07-4e97-9d5f-0741e74f8043/_2020-10-11__9.07.49.png

    4. 🚒[중요] 꼭 다음 사항을 확인해주세요.

      • Group: com.sparta
      • Artifact: week03
      • Type: Gradle
      • Language: Java
      • Java Version: 8

      특히 Type, Language, Java version 중 하나라도 다르면 정상적으로 실행이 되지 않습니다. 꼭 정상입력 확인해주세요!

      https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3084efcf-6d4c-49fc-8e09-d2b596900095/_2020-10-11__9.08.51.png

    5. 검색창을 클릭하고, 다음을 차례대로 검색한 뒤 엔터를 눌러주세요. 검색이 끝난 뒤 아래 캡처의 우측과 같이 5개의 요소가 포함되어있으면 완료된 것입니다.

      • Lombok
      • Spring Web
      • Spring Data JPA
      • H2 Database
      • MySQL Driver

      https://s3-us-west-2.amazonaws.com/secure.notion-static.com/99523adc-19db-4351-8c93-08fcc2144765/_2020-10-11__9.16.34.png

    6. Finish를 클릭합니다.

      https://s3-us-west-2.amazonaws.com/secure.notion-static.com/87bd5a10-e840-4c85-a0d4-40a2b29b9957/Untitled.png

    7. 잠시 기다리면, 아래 화면이 완성될 것입니다.

      https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6c3d00a8-5e48-4133-badb-f5848061e2eb/Untitled.png

  • API 설계하기 (CRUD)

03. [3주차] Repository 만들기

  • Memo 클래스 만들기

    • 메모는 1) 익명의 작성자 이름(username), 2) 메모 내용(contents) 으로 이루어져 있습니다.
    • domain 패키지를 만듭니다.
    • [코드스니펫] Memo.java

      @NoArgsConstructor // 기본생성자를 만듭니다.
      @Getter
      @Entity // 테이블과 연계됨을 스프링에게 알려줍니다.
      public class Memo extends Timestamped { // 생성,수정 시간을 자동으로 만들어줍니다.
          @GeneratedValue(strategy = GenerationType.AUTO)
          @Id
          private Long id;
      
          @Column(nullable = false)
          private String username;
      
          @Column(nullable = false)
          private String contents;
      
          public Memo(String username, String contents) {
              this.username = username;
              this.contents = contents;
          }
      
          public Memo(MemoRequestDto requestDto) {
              this.username = requestDto.getUsername();
              this.contents = requestDto.getContents();
          }
      }
      
    • [코드스니펫] Timestamped.java

      @MappedSuperclass // Entity가 자동으로 컬럼으로 인식합니다.
      @EntityListeners(AuditingEntityListener.class) // 생성/변경 시간을 자동으로 업데이트합니다.
      public class Timestamped {
      
          @CreatedDate
          private LocalDateTime createdAt;
      
          @LastModifiedDate
          private LocalDateTime modifiedAt;
      }
      
  • MemoRepository 인터페이스 만들기

    • ID가 Long 타입입니다. Repository는 어떻게 생겼을까요?
    • [코드스니펫] MemoRepository.java

      public interface MemoRepository extends JpaRepository<Memo, Long> {
      	List<Memo> findAllByOrderByModifiedAtDesc();
      }
      
    • [코드스니펫] JPA 공식 홈페이지 살펴보기

      https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods
      
  • 연습퀴즈 - MemoRequestDto 클래스 만들기

    Memo.java에 뜨는 에러를 해결하려면 MemoRequestDto.java를 만들어야 합니다. Memo 클래스의 코드를 바탕으로 MemoRequestDto.java를 만들어보세요.

    • 정답

      @Getter
      public class MemoRequestDto {
          private String username;
          private String contents;
      }
      

04. [3주차] Service 만들기

  • 전체 그림 다시 보기

    https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0f40eb9b-9907-4600-9bdc-3437c36cff54/Untitled.png

    • Controller - Service - Repository 3계층이 존재한다는 것, 기억하시죠?
    • 이번 시간에는 Service 계층을 만들어보겠습니다.
  • MemoService 클래스 만들기

    1. src > main > java > com.sparta.week03 에 service 패키지를 만듭니다.
    2. 해당 패키지 아래에 MemoService.java 파일을 만듭니다.
    3. [코드스니펫] MemoService.java

      @RequiredArgsConstructor
      @Service
      public class MemoService {
      
          private final MemoRepository memoRepository;
      
          @Transactional
          public Long update(Long id, MemoRequestDto requestDto) {
              Memo memo = memoRepository.findById(id).orElseThrow(
                      () -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
              );
              memo.update(requestDto);
              return memo.getId();
          }
      }
      
  • update 기능 만들기

    • [코드스니펫] Memo.java에 update 메소드 추가하기

      public void update(MemoRequestDto requestDto) {
          this.username = requestDto.getUsername();
          this.contents = requestDto.getContents();
      }
      

05. [3주차] Controller 만들기

  • 전체 그림 다시보기

    https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0f40eb9b-9907-4600-9bdc-3437c36cff54/Untitled.png

    • Controller - Service - Repository 3계층이 존재한다는 것, 기억하시죠?
    • 이번 시간에는 Controller 계층을 만들어보겠습니다.
  • API 완성하기
  • ARC로 기능 확인하기
  • 연습퀴즈 - Update API 만들어보기

    메모의 정보를 받아 DB의 데이터를 변경하는 메소드를 만들어보세요. 힌트) Create 와 유사합니다.

    • 정답

      @PutMapping("/api/memos/{id}")
      public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
          memoService.update(id, requestDto);
          return id;
      }
      

06. [3주차] HTML, CSS 기초

  • HTML, CSS, Javascript 의 구분
    • HTML은 뼈대고, CSS는 꾸며주는 녀석입니다. 바로 살펴볼까요?
    • https://naver.com 에서 <head></head> 녀석을 날려보겠습니다.
  • HTML의 기초

    • HTML의 특징

      • 여는 태그, 닫는 태그가 쌍으로 존재합니다.
      • 교차가 불가능합니다.

        (O) <h1><span>타이틀</span>입니다.</h1>
        
        (X) <span><h1>타이틀</span>입니다.</h1>
        
      • head, body 태그로 구분됩니다.
        • head 태그는 CSS, Javascript 코드를 포함하고,
        • body 태그는 뼈대 전체를 포함합니다.
    • 대표적인 HTML 태그들

      • h1~h6

        → headline의 약자, 신문 제목 같은 녀석이에요.

      • div

        → divison, 나누는 녀석이에요. 투명 비닐봉투!

      • p

        → paragraph, 신문 기사 단락과 같은 텍스트 내용을 담습니다.

      • ul, ol, li

        → (un)ordered list, list에 해당합니다. bullet point.

      • span

        → 글을 중간중간 잘라내서 색을 입혀준다든지 할 때 사용합니다.

      • table, th, tr, td

        → 표 입니다! 엑셀 같은 표를 그릴 때 사용합니다.

      • img

        → 이미지를 나타낼 때 사용합니다.

      그 외에도 무수히 많습니다!

    • 더 공부하고 싶다면?

      • [코드스니펫] 코드카데미 HTML

        https://www.codecademy.com/learn/learn-html
        
      • [코드스니펫] 생활코딩 HTML

        https://opentutorials.org/course/2039
        
  • CSS의 기초

    • 문법

      • head > style 태그 안에 작성합니다.
      • 세미콜론(;)으로 마무리합니다.
      .wrap {
          width: 538px;
          margin: 10px auto;
      }
      
      #contents {
          width: 538px;
      }
      
      .area-write {
          position: relative;
          width: 538px;
      }
      
    • 선택자(id, class)

      • 꾸미려면 가리켜야 하겠죠? 가리키는 방법은 두 가지가 있습니다.
      • id: HTML 파일을 통틀어 단 하나만 존재합니다. #으로 표시합니다. (#contents)
      • class: 중복 적용이 가능합니다. .으로 표시합니다. (.area-write)
  • 타임라인 페이지를 미리 한 번 살펴볼까요?

    1. src > main > resources > static 에 index.html 파일을 만듭니다.
    2. 아래 코드스니펫 내용을 복사/붙여넣기 합니다.

      • [코드스니펫] index.html

        <!DOCTYPE html>
        <html lang="en">
          <head>
            <meta charset="UTF-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <title>Timeline Service</title>
        
            <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
            <link
              href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@500&display=swap"
              rel="stylesheet"
            />
            <script>
              $(document).ready(function () {
                getMessages();
              });
        
              function getMessages() {
                $("#cards-box").empty();
                $.ajax({
                  type: "GET",
                  url: "/api/memos",
                  data: {},
                  success: function (response) {
                    for (let i = 0; i < response.length; i++) {
                      let message = response[i];
                      let id = message["id"];
                      let username = message["username"];
                      let contents = message["contents"];
                      let modifiedAt = message["modifiedAt"];
                      addHTML(id, username, contents, modifiedAt);
                    }
                  },
                });
              }
        
              function addHTML(id, username, contents, modifiedAt) {
                let tempHtml = makeMessage(id, username, contents, modifiedAt);
                $("#cards-box").append(tempHtml);
              }
        
              function makeMessage(id, username, contents, modifiedAt, i) {
                return `<div class="card">
                                <!-- date/username 영역 -->
                                <div class="metadata">
                                    <div class="date">
                                        ${modifiedAt}
                                    </div>
                                    <div id="${id}-username" class="username">
                                        ${username}
                                    </div>
                                </div>
                                <!-- contents 조회/수정 영역-->
                                <div class="contents">
                                    <div id="${id}-contents" class="text">
                                        ${contents}
                                    </div>
                                    <div id="${id}-editarea" class="edit">
                                        <textarea id="${id}-textarea" class="te-edit" name="" id="" cols="30" rows="5"></textarea>
                                    </div>
                                </div>
                                <!-- 버튼 영역-->
                                <div class="footer">
                                    <img id="${id}-edit" class="icon-start-edit" src="images/edit.png" alt="" onclick="editPost('${id}')">
                                    <img id="${id}-delete" class="icon-delete" src="images/delete.png" alt="" onclick="deleteOne('${id}')">
                                    <img id="${id}-submit" class="icon-end-edit" src="images/done.png" alt="" onclick="submitEdit('${id}')">
                                </div>
                            </div>`;
              }
        
              function isValidContents(contents) {
                if (contents == "") {
                  alert("내용을 입력해주세요");
                  return false;
                }
                if (contents.trim().length > 140) {
                  alert("공백 포함 140자 이하로 입력해주세요");
                  return false;
                }
                return true;
              }
        
              function genRandomName(length) {
                let result = "";
                let characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
                let charactersLength = characters.length;
                for (let i = 0; i < length; i++) {
                  let number = Math.random() * charactersLength;
                  let index = Math.floor(number);
                  result += characters.charAt(index);
                }
                return result;
              }
        
              function writePost() {
                let contents = $("#contents").val();
        
                if (isValidContents(contents) == false) {
                  return;
                }
        
                let username = genRandomName(10);
                let data = { username: username, contents: contents };
        
                $.ajax({
                  type: "POST",
                  url: "/api/memos",
                  contentType: "application/json",
                  data: JSON.stringify(data),
                  success: function (response) {
                    alert("메시지가 성공적으로 작성되었습니다.");
                    window.location.reload();
                  },
                });
              }
        
              function editPost(id) {
                showEdits(id);
                let contents = $(`#${id}-contents`).text().trim();
                $(`#${id}-textarea`).val(contents);
              }
        
              function showEdits(id) {
                $(`#${id}-editarea`).show();
                $(`#${id}-submit`).show();
                $(`#${id}-delete`).show();
        
                $(`#${id}-contents`).hide();
                $(`#${id}-edit`).hide();
              }
        
              function hideEdits(id) {
                $(`#${id}-editarea`).hide();
                $(`#${id}-submit`).hide();
                $(`#${id}-delete`).hide();
        
                $(`#${id}-contents`).show();
                $(`#${id}-edit`).show();
              }
        
              function submitEdit(id) {
                let username = $(`#${id}-username`).text().trim();
                let contents = $(`#${id}-textarea`).val().trim();
                if (isValidContents(contents) == false) {
                  return;
                }
                let data = { username: username, contents: contents };
        
                $.ajax({
                  type: "PUT",
                  url: `/api/memos/${id}`,
                  contentType: "application/json",
                  data: JSON.stringify(data),
                  success: function (response) {
                    alert("메시지 변경에 성공하였습니다.");
                    window.location.reload();
                  },
                });
              }
        
              function deleteOne(id) {
                $.ajax({
                  type: "DELETE",
                  url: `/api/memos/${id}`,
                  success: function (response) {
                    alert("메시지 삭제에 성공하였습니다.");
                    window.location.reload();
                  },
                });
              }
            </script>
        
            <style>
              @import url(//spoqa.github.io/spoqa-han-sans/css/SpoqaHanSans-kr.css);
        
              body {
                margin: 0px;
              }
        
              .area-edit {
                display: none;
              }
        
              .wrap {
                width: 538px;
                margin: 10px auto;
              }
        
              #contents {
                width: 538px;
              }
        
              .area-write {
                position: relative;
                width: 538px;
              }
        
              .area-write img {
                cursor: pointer;
                position: absolute;
                width: 22.2px;
                height: 18.7px;
                bottom: 15px;
                right: 17px;
              }
        
              .background-header {
                position: fixed;
                z-index: -1;
                top: 0px;
                width: 100%;
                height: 428px;
                background-color: #339af0;
              }
        
              .background-body {
                position: fixed;
                z-index: -1;
                top: 428px;
                height: 100%;
                width: 100%;
                background-color: #dee2e6;
              }
        
              .header {
                margin-top: 50px;
              }
        
              .header h2 {
                /*font-family: 'Noto Sans KR', sans-serif;*/
                height: 33px;
                font-size: 42px;
                font-weight: 500;
                font-stretch: normal;
                font-style: normal;
                line-height: 0.79;
                letter-spacing: -0.5px;
                text-align: center;
                color: #ffffff;
              }
        
              .header p {
                margin: 40px auto;
                width: 217px;
                height: 48px;
                font-family: "Noto Sans KR", sans-serif;
                font-size: 16px;
                font-weight: 500;
                font-stretch: normal;
                font-style: normal;
                line-height: 1.5;
                letter-spacing: -1.12px;
                text-align: center;
                color: #ffffff;
              }
        
              textarea.field {
                width: 502px !important;
                height: 146px;
                border-radius: 5px;
                background-color: #ffffff;
                border: none;
                padding: 18px;
                resize: none;
              }
        
              textarea.field::placeholder {
                width: 216px;
                height: 16px;
                font-family: "Noto Sans KR", sans-serif;
                font-size: 16px;
                font-weight: normal;
                font-stretch: normal;
                font-style: normal;
                line-height: 1;
                letter-spacing: -0.96px;
                text-align: left;
                color: #868e96;
              }
        
              .card {
                width: 538px;
                border-radius: 5px;
                background-color: #ffffff;
                margin-bottom: 12px;
              }
        
              .card .metadata {
                position: relative;
                display: flex;
                font-family: "Spoqa Han Sans";
                font-size: 11px;
                font-weight: normal;
                font-stretch: normal;
                font-style: normal;
                line-height: 1;
                letter-spacing: -0.77px;
                text-align: left;
                color: #adb5bd;
                height: 14px;
                padding: 10px 23px;
              }
        
              .card .metadata .date {
              }
        
              .card .metadata .username {
                margin-left: 20px;
              }
        
              .contents {
                padding: 0px 23px;
                word-wrap: break-word;
                word-break: break-all;
              }
        
              .contents div.edit {
                display: none;
              }
        
              .contents textarea.te-edit {
                border-right: none;
                border-top: none;
                border-left: none;
                resize: none;
                border-bottom: 1px solid #212529;
                width: 100%;
                font-family: "Spoqa Han Sans";
              }
        
              .footer {
                position: relative;
                height: 40px;
              }
        
              .footer img.icon-start-edit {
                cursor: pointer;
                position: absolute;
                bottom: 14px;
                right: 55px;
                width: 18px;
                height: 18px;
              }
        
              .footer img.icon-end-edit {
                cursor: pointer;
                position: absolute;
                display: none;
                bottom: 14px;
                right: 55px;
                width: 20px;
                height: 15px;
              }
        
              .footer img.icon-delete {
                cursor: pointer;
                position: absolute;
                bottom: 12px;
                right: 19px;
                width: 14px;
                height: 18px;
              }
        
              #cards-box {
                margin-top: 12px;
              }
            </style>
          </head>
        
          <body>
            <div class="background-header"></div>
            <div class="background-body"></div>
            <div class="wrap">
              <div class="header">
                <h2>Timeline Service</h2>
                <p>공유하고 싶은 소식을 입력해주세요. 24시간이 지난 뒤에는 사라집니다.</p>
              </div>
              <div class="area-write">
                <textarea
                  class="field"
                  placeholder="공유하고 싶은 소식을 입력해주세요"
                  name="contents"
                  id="contents"
                  cols="30"
                  rows="10"
                ></textarea>
                <!--            <button class="btn btn-danger" onclick="writePost()">작성하기</button>-->
                <img src="images/send.png" alt="" onclick="writePost()" />
              </div>
              <div id="cards-box" class="area-read">
                <div class="card">
                  <!-- date/username 영역 -->
                  <div class="metadata">
                    <div class="date">October 10, 2020</div>
                    <div class="username">anonymous</div>
                  </div>
                  <!-- contents 조회/수정 영역-->
                  <div class="contents">
                    <div id="1-contents" class="text">
                      dsafnkalfklewakflekelafkleajfkleafkldsankflenwaklfnekwlafneklwanfkelawnfkelanfkleanfklew
                    </div>
                    <div id="1-editarea" class="edit">
                      <textarea
                        id="1-textarea"
                        class="te-edit"
                        name=""
                        id=""
                        cols="30"
                        rows="5"
                      ></textarea>
                    </div>
                  </div>
                  <!-- 버튼 영역-->
                  <div class="footer">
                    <img
                      id="1-edit"
                      class="icon-start-edit"
                      src="images/edit.png"
                      alt=""
                      onclick="editPost('1')"
                    />
                    <img
                      id="1-delete"
                      class="icon-delete"
                      src="images/delete.png"
                      alt=""
                      onclick="deleteOne('1')"
                    />
                    <img
                      id="1-submit"
                      class="icon-end-edit"
                      src="images/done.png"
                      alt=""
                      onclick="submitEdit('1')"
                    />
                  </div>
                </div>
              </div>
            </div>
          </body>
        </html>
        
    3. static 폴더에 images 폴더를 만듭니다.
    4. 이미지를 다운로드 받고, images 폴더에 넣습니다.

      • [코드스니펫] delete 이미지

        https://s3.ap-northeast-2.amazonaws.com/materials.spartacodingclub.kr/spring/week03/delete.png
        
      • [코드스니펫] done 이미지

        https://s3.ap-northeast-2.amazonaws.com/materials.spartacodingclub.kr/spring/week03/done.png
        
      • [코드스니펫] edit 이미지

        https://s3.ap-northeast-2.amazonaws.com/materials.spartacodingclub.kr/spring/week03/edit.png
        
      • [코드스니펫] send 이미지

        https://s3.ap-northeast-2.amazonaws.com/materials.spartacodingclub.kr/spring/week03/send.png
        

07. [3주차] Javascript 기초 - 1

  • Javascript 란?
    • 브라우저를 살아 숨쉬게 만드는 친구입니다.
    • 클릭, 마우스 오버 시 색 변화, 숨기기, 나타내기, 등등 수많은 일을 할 수 있답니다 😎
  • 크롬 개발자 도구
    • Javascript 공부, 아니 웹서비스 개발을 하는데 필수적인 녀석입니다.
    • F12 키를 누르면 개발자 도구가 뜹니다! console 탭을 눌러볼게요.
  • Javascript 기초 문법

    • 변수

      • [코드스니펫] 자바스크립트 변수

        let a = 3; // 변수를 처음 선언할 때 let을 써줍니다. 자료형은 써주지 않아도 되어요.
        let b = 2;
        console.log(a + b); // System.out.println()과 같은 녀석입니다.
        b = 7;
        console.log(a + b);
        
    • 자료형 (문자, 숫자, boolean, 리스트, 딕셔너리)

      • [코드스니펫] 자바스크립트 문자, 숫자

        let name = "bknam";
        let course = "웹개발의 봄 Spring"; // 자바와 다르게 홑/쌍따옴표 상관없습니다.
        let num = 10;
        console.log(num + name); // 문자 + 숫자 하면 둘 모두를 문자로 묶습니다.
        
      • [코드스니펫] 자바스크립트 boolean

        let age1 = 18;
        let age2 = 20;
        let isAdult = age1 > 19;
        console.log(isAdult); // false
        isAdult = age2 > 19;
        console.log(isAdult); // true
        
      • [코드스니펫] 자바스크립트 리스트

        let fruits = ["사과", "딸기", "수박"]; // List 보다 편하게 사용할 수 있습니다.
        console.log(fruits[0]);
        console.log(fruits[1]);
        console.log(fruits[2]);
        
      • [코드스니펫] 자바스크립트 딕셔너리

        let course = {
          title: "웹개발의 봄, Spring",
          tutor: "남병관",
        };
        
        console.log(course);
        
    • 반복문

      • [코드스니펫] 자바스크립트 반복문

        let fruits = ["사과", "딸기", "수박"]; // List 보다 편하게 사용할 수 있습니다.
        for (let i = 0; i < fruits.length; i++) {
          let fruit = fruits[i];
          console.log(fruit);
        }
        

08. [3주차] Javascript 기초 - 2

  • Javascript 기초 문법

    • 조건문

      • [코드스니펫] 자바스크립트 조건문

        let fruits = ["사과", "딸기", "수박"]; // List 보다 편하게 사용할 수 있습니다.
        for (let i = 0; i < fruits.length; i++) {
          let fruit = fruits[i];
          console.log(fruit == "수박");
        }
        
    • 함수

      • [코드스니펫] 자바스크립트 함수

        function sample() {
          alert("얼럿!");
        }
        
    • 백틱

      • [코드스니펫] 자바스크립트 백틱 연습

        let name = "내 이름";
        let text = `${name}님의 스프링 5주 완주를 축하합니다!`;
        console.log(text);
        
  • 연습 퀴즈 - 과일 개수 세기

    Javascript 배열 안에서 과일이 몇 개인지 세는 함수를 만들어보겠습니다. 다음 실행 결과를 만들 수 있으면 됩니다.

    • let count = countFruit(‘감’); console.log(count); // 배열에 들어있는 감의 개수 인쇄

    • 정답

      let fruits = ["사과", "딸기", "수박", "", "", "딸기", ""];
      
      function countFruit(name) {
        let result = 0;
        for (let i = 0; i < fruits.length; i++) {
          let fruit = fruits[i];
          if (fruit == name) {
            result += 1;
          }
        }
        return result;
      }
      
      let count = countFruit("");
      console.log(count); // 배열에 들어있는 감의 개수 인쇄
      

09. [3주차] jQuery 기초 - 1

  • jQuery 란?
    • jQuery는, 미리 작성된 자바스크립트 함수 모음집입니다.
    • 되게 많이들 쓰는 HTML, CSS 조작 함수를 미리 만들어서 제공해주는 것이죠!
    • 우리는 다 스스로 만들 필요 없이 사용법만 알면 됩니다. 😎
    • https://www.w3schools.com/jquery/jquery_get_started.asp
  • [코드스니펫] jQuery 임포트 (head 태그 사이에!)

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    
  • jQuery 사용하는 법

    • $ 로 시작하고, 괄호 안에 선택자로 대상을 적으면 됩니다.

      $("#contents").hide();
      
  • 연습 장소로 들어가보기

    • 개발자 도구 > 콘솔 에서 jQuery 를 사용하려면 해당 웹사이트에 이미 임포트 되어 있어야 합니다.
    • [코드스니펫] jQuery 놀이터 들어가기

      [http://spartacodingclub.shop](http://spartacodingclub.shop)
      
  • 숨기기 / 나타내기

    • [코드스니펫] HTML 나타내기

      $("#post-box").show();
      
    • [코드스니펫] HTML 숨기기

      $("#post-box").hide();
      

10. [3주차] jQuery 기초 - 2

  • input 값 가져오기, 넣기

    • [코드스니펫] input 값 가져오기

      $("#post-url").val();
      
    • [코드스니펫] input 값 넣기

      $("#post-url").val("new text");
      
  • HTML 없애기, 추가하기

    • [코드스니펫] HTML 없애기

      $("#cards-box").empty();
      
    • [코드스니펫] HTML 추가하기

      $("#cards-box").append(`<div class="card">
          <img class="card-img-top"
               src="https://www.eurail.com/content/dam/images/eurail/Italy%20OCP%20Promo%20Block.adaptive.767.1535627244182.jpg"
               alt="Card image cap">
          <div class="card-body">
              <a href="#" class="card-title">여기 기사 제목이 들어가죠</a>
              <p class="card-text">기사의 요약 내용이 들어갑니다. 동해물과 백두산이 마르고 닳도록 하느님이 보우하사 우리나라만세 무궁화 삼천리 화려강산...</p>
              <p class="card-text comment">여기에 코멘트가 들어갑니다.</p>
          </div>
      </div>`);
      

11. [3주차] 클라이언트 설계하기

  • 필요한 기능 살펴보기
  • 시작코드

12. [3주차] 메모 생성하기 - writePost 함수

  • 개발 스펙 확인
  • [코드스니펫] 사용자가 입력한 메모 내용 확인하기
  • [**코드스니펫] 작성 내용 확인하기**
  • [코드스니펫] 랜덤한 username 만들기
  • [코드스니펫] 전달할 data 를 JSON으로 만들기
  • [코드스니펫] POST API 사용해서 메모 신규 생성하기
  • [코드스니펫] writePost 함수 완성

13. [3주차] 메모 조회하기 - getMessages 함수 - 1

  • 개발 스펙 확인
  • 메모 조회하기
  • [코드스니펫] 기존 메모 제거하기
  • [코드스니펫] GET API 사용해서 메모 목록 불러오기

14. [3주차] 메모 조회하기 - getMessages 함수 - 2

  • [코드스니펫] 메모 마다 HTML 만들고 붙이는 함수 만들기
  • Timestamped, Week03Application 수정하기
  • [코드스니펫] 반복문 안에서 addHTML 호출
  • [코드스니펫] getMessages, addHTML 완성

15. [3주차] 메모 변경하기 - submitEdit 함수

  • 개발 스펙 확인
  • [코드스니펫] addHtml 리뷰
  • [코드스니펫] 작성 대상 메모의 username과 contents 를 확인
  • [코드스니펫] 작성한 메모가 올바른지 확인
  • [코드스니펫] 전달할 data 를 JSON으로 바꾸기
  • [코드스니펫] PUT /api/memos/{id} 에 data를 전달

    $.ajax({
      type: "PUT",
      url: `/api/memos/${id}`,
      contentType: "application/json",
      data: JSON.stringify(data),
      success: function (response) {
        alert("메시지 변경에 성공하였습니다.");
        window.location.reload();
      },
    });
    
  • [코드스니펫] submitEdit 함수 완성

    // 메모를 수정합니다.
    function submitEdit(id) {
      // 1. 작성 대상 메모의 username과 contents 를 확인합니다.
      let username = $(`#${id}-username`).text().trim();
      let contents = $(`#${id}-textarea`).val().trim();
    
      // 2. 작성한 메모가 올바른지 isValidContents 함수를 통해 확인합니다.
      if (isValidContents(contents) == false) {
        return;
      }
    
      // 3. 전달할 data JSON으로 만듭니다.
      let data = { username: username, contents: contents };
    
      // 4. PUT /api/memos/{id} 에 data를 전달합니다.
      $.ajax({
        type: "PUT",
        url: `/api/memos/${id}`,
        contentType: "application/json",
        data: JSON.stringify(data),
        success: function (response) {
          alert("메시지 변경에 성공하였습니다.");
          window.location.reload();
        },
      });
    }
    

16. [3주차] 메모 삭제하기 - deleteOne 함수

  • 개발 스펙 확인
    1. DELETE API 사용해서 메모 삭제하기
  • [코드스니펫] DELETE API 사용해서 메모 삭제하기

    function deleteOne(id) {
      $.ajax({
        type: "DELETE",
        url: `/api/memos/${id}`,
        success: function (response) {
          alert("메시지 삭제에 성공하였습니다.");
          window.location.reload();
        },
      });
    }
    
  • [**코드스니펫] 클라이언트 완성 코드**

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Timeline Service</title>
    
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
        <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@500&display=swap" rel="stylesheet">
    
        <style>
            @import url(//spoqa.github.io/spoqa-han-sans/css/SpoqaHanSans-kr.css);
    
            body {
                margin: 0px;
            }
    
            .area-edit {
                display: none;
            }
    
            .wrap {
                width: 538px;
                margin: 10px auto;
            }
    
            #contents {
                width: 538px;
            }
    
            .area-write {
                position: relative;
                width: 538px;
            }
    
            .area-write img {
                cursor: pointer;
                position: absolute;
                width: 22.2px;
                height: 18.7px;
                bottom: 15px;
                right: 17px;
            }
    
            .background-header {
                position: fixed;
                z-index: -1;
                top: 0px;
                width: 100%;
                height: 428px;
                background-color: #339af0;
            }
    
            .background-body {
                position: fixed;
                z-index: -1;
                top: 428px;
                height: 100%;
                width: 100%;
                background-color: #dee2e6;
            }
    
            .header {
                margin-top: 50px;
            }
    
            .header h2 {
                /*font-family: 'Noto Sans KR', sans-serif;*/
                height: 33px;
                font-size: 42px;
                font-weight: 500;
                font-stretch: normal;
                font-style: normal;
                line-height: 0.79;
                letter-spacing: -0.5px;
                text-align: center;
                color: #ffffff;
            }
    
            .header p {
                margin: 40px auto;
                width: 217px;
                height: 48px;
                font-family: 'Noto Sans KR', sans-serif;
                font-size: 16px;
                font-weight: 500;
                font-stretch: normal;
                font-style: normal;
                line-height: 1.5;
                letter-spacing: -1.12px;
                text-align: center;
                color: #ffffff;
            }
    
            textarea.field {
                width: 502px !important;
                height: 146px;
                border-radius: 5px;
                background-color: #ffffff;
                border: none;
                padding: 18px;
                resize: none;
            }
    
            textarea.field::placeholder {
                width: 216px;
                height: 16px;
                font-family: 'Noto Sans KR', sans-serif;
                font-size: 16px;
                font-weight: normal;
                font-stretch: normal;
                font-style: normal;
                line-height: 1;
                letter-spacing: -0.96px;
                text-align: left;
                color: #868e96;
            }
    
            .card {
                width: 538px;
                border-radius: 5px;
                background-color: #ffffff;
                margin-bottom: 12px;
            }
    
            .card .metadata {
                position: relative;
                display: flex;
                font-family: 'Spoqa Han Sans';
                font-size: 11px;
                font-weight: normal;
                font-stretch: normal;
                font-style: normal;
                line-height: 1;
                letter-spacing: -0.77px;
                text-align: left;
                color: #adb5bd;
                height: 14px;
                padding: 10px 23px;
            }
    
            .card .metadata .date {
    
            }
    
            .card .metadata .username {
                margin-left: 20px;
            }
    
            .contents {
                padding: 0px 23px;
                word-wrap: break-word;
                word-break: break-all;
            }
    
            .contents div.edit {
                display: none;
            }
    
            .contents textarea.te-edit {
                border-right: none;
                border-top: none;
                border-left: none;
                resize: none;
                border-bottom: 1px solid #212529;
                width: 100%;
                font-family: 'Spoqa Han Sans';
            }
    
            .footer {
                position: relative;
                height: 40px;
            }
    
            .footer img.icon-start-edit {
                cursor: pointer;
                position: absolute;
                bottom: 14px;
                right: 55px;
                width: 18px;
                height: 18px;
            }
    
            .footer img.icon-end-edit {
                cursor: pointer;
                position: absolute;
                display: none;
                bottom: 14px;
                right: 55px;
                width: 20px;
                height: 15px;
            }
    
            .footer img.icon-delete {
                cursor: pointer;
                position: absolute;
                bottom: 12px;
                right: 19px;
                width: 14px;
                height: 18px;
            }
    
            #cards-box {
                margin-top: 12px;
            }
        </style>
        <script>
            // 미리 작성된 영역 - 수정하지 않으셔도 됩니다.
            // 사용자가 내용을 올바르게 입력하였는지 확인합니다.
            function isValidContents(contents) {
                if (contents == '') {
                    alert('내용을 입력해주세요');
                    return false;
                }
                if (contents.trim().length > 140) {
                    alert('공백 포함 140자 이하로 입력해주세요');
                    return false;
                }
                return true;
            }
    
            // 익명의 username을 만듭니다.
            function genRandomName(length) {
                let result = '';
                let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
                let charactersLength = characters.length;
                for (let i = 0; i < length; i++) {
                    let number = Math.random() * charactersLength;
                    let index = Math.floor(number);
                    result += characters.charAt(index);
                }
                return result;
            }
    
           // 수정 버튼을 눌렀을 때, 기존 작성 내용을 textarea 에 전달합니다.
           // 숨길 버튼을 숨기고, 나타낼 버튼을 나타냅니다.
            function editPost(id) {
                showEdits(id);
                let contents = $(`#${id}-contents`).text().trim();
                $(`#${id}-textarea`).val(contents);
            }
    
            function showEdits(id) {
                $(`#${id}-editarea`).show();
                $(`#${id}-submit`).show();
                $(`#${id}-delete`).show();
    
                $(`#${id}-contents`).hide();
                $(`#${id}-edit`).hide();
            }
            ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
            // 여기서부터 코드를 작성해주시면 됩니다.
    
            $(document).ready(function () {
                // HTML 문서를 로드할 때마다 실행합니다.
                getMessages();
            })
    
            // 메모를 불러와서 보여줍니다.
            function getMessages() {
                // 1. 기존 메모 내용을 지웁니다.
                $('#cards-box').empty();
                // 2. 메모 목록을 불러와서 HTML로 붙입니다.
                $.ajax({
                    type: 'GET',
                    url: '/api/memos',
                    success: function (response) {
                        for (let i = 0; i < response.length; i++) {
                            let message = response[i];
                            let id = message['id'];
                            let username = message['username'];
                            let contents = message['contents'];
                            let modifiedAt = message['modifiedAt'];
                            addHTML(id, username, contents, modifiedAt);
                        }
                    }
                })
            }
    
            // 메모 하나를 HTML로 만들어서 body 태그 내 원하는 곳에 붙입니다.
            function addHTML(id, username, contents, modifiedAt) {
                // 1. HTML 태그를 만듭니다.
                let tempHtml = `<div class="card">
                    <!-- date/username 영역 -->
                    <div class="metadata">
                        <div class="date">
                            ${modifiedAt}
                        </div>
                        <div id="${id}-username" class="username">
                            ${username}
                        </div>
                    </div>
                    <!-- contents 조회/수정 영역-->
                    <div class="contents">
                        <div id="${id}-contents" class="text">
                            ${contents}
                        </div>
                        <div id="${id}-editarea" class="edit">
                            <textarea id="${id}-textarea" class="te-edit" name="" id="" cols="30" rows="5"></textarea>
                        </div>
                    </div>
                    <!-- 버튼 영역-->
                    <div class="footer">
                        <img id="${id}-edit" class="icon-start-edit" src="images/edit.png" alt="" onclick="editPost('${id}')">
                        <img id="${id}-delete" class="icon-delete" src="images/delete.png" alt="" onclick="deleteOne('${id}')">
                        <img id="${id}-submit" class="icon-end-edit" src="images/done.png" alt="" onclick="submitEdit('${id}')">
                    </div>
                </div>`;
                // 2. #cards-box 에 HTML을 붙인다.
                $('#cards-box').append(tempHtml);
            }
    
            // 메모를 생성합니다.
            function writePost() {
                // 1. 작성한 메모를 불러옵니다.
                let contents = $('#contents').val();
    
                // 2. 작성한 메모가 올바른지 isValidContents 함수를 통해 확인합니다.
                if (isValidContents(contents) == false) {
                    return;
                }
                // 3. genRandomName 함수를 통해 익명의 username을 만듭니다.
                let username = genRandomName(10);
    
                // 4. 전달할 data JSON으로 만듭니다.
                let data = {'username': username, 'contents': contents};
    
                // 5. POST /api/memos 에 data를 전달합니다.
                $.ajax({
                    type: "POST",
                    url: "/api/memos",
                    contentType: "application/json",
                    data: JSON.stringify(data),
                    success: function (response) {
                        alert('메시지가 성공적으로 작성되었습니다.');
                        window.location.reload();
                    }
                });
            }
    
            // 메모를 수정합니다.
            function submitEdit(id) {
                // 1. 작성 대상 메모의 username과 contents 를 확인합니다.
                let username = $(`#${id}-username`).text().trim();
                let contents = $(`#${id}-textarea`).val().trim();
    
                // 2. 작성한 메모가 올바른지 isValidContents 함수를 통해 확인합니다.
                if (isValidContents(contents) == false) {
                    return;
                }
    
                // 3. 전달할 data JSON으로 만듭니다.
                let data = {'username': username, 'contents': contents};
    
                // 4. PUT /api/memos/{id} 에 data를 전달합니다.
                $.ajax({
                    type: "PUT",
                    url: `/api/memos/${id}`,
                    contentType: "application/json",
                    data: JSON.stringify(data),
                    success: function (response) {
                        alert('메시지 변경에 성공하였습니다.');
                        window.location.reload();
                    }
                });
            }
    
            // 메모를 삭제합니다.
            function deleteOne(id) {
                // 1. DELETE /api/memos/{id} 에 요청해서 메모를 삭제합니다.
                $.ajax({
                    type: "DELETE",
                    url: `/api/memos/${id}`,
                    success: function (response) {
                        alert('메시지 삭제에 성공하였습니다.');
                        window.location.reload();
                    }
                })
            }
        </script>
    </head>
    
    <body>
    <div class="background-header">
    
    </div>
    <div class="background-body">
    
    </div>
    <div class="wrap">
        <div class="header">
            <h2>Timeline Service</h2>
            <p>
                공유하고 싶은 소식을 입력해주세요.
                24시간이 지난 뒤에는 사라집니다.
            </p>
        </div>
        <div class="area-write">
            <textarea class="field" placeholder="공유하고 싶은 소식을 입력해주세요" name="contents" id="contents" cols="30"
                      rows="10"></textarea>
            <!--            <button class="btn btn-danger" onclick="writePost()">작성하기</button>-->
            <img src="send.png" alt="" onclick="writePost()">
        </div>
        <div id="cards-box" class="area-read">
            <div class="card">
                <!-- date/username 영역 -->
                <div class="metadata">
                    <div class="date">
                        October 10, 2020
                    </div>
                    <div class="username">
                        anonymous
                    </div>
                </div>
                <!-- contents 조회/수정 영역-->
                <div class="contents">
                    <div id="1-contents" class="text">
                        dsafnkalfklewakflekelafkleajfkleafkldsankflenwaklfnekwlafneklwanfkelawnfkelanfkleanfklew
                    </div>
                    <div id="1-editarea" class="edit">
                        <textarea id="1-textarea" class="te-edit" name="" id="" cols="30" rows="5"></textarea>
                    </div>
                </div>
                <!-- 버튼 영역-->
                <div class="footer">
                    <img id="1-edit" class="icon-start-edit" src="edit.png" alt="" onclick="editPost('1')">
                    <img id="1-delete" class="icon-delete" src="delete.png" alt="" onclick="deleteOne('1')">
                    <img id="1-submit" class="icon-end-edit" src="done.png" alt="" onclick="submitEdit('1')">
                </div>
            </div>
            <div class="card">
                <!-- date/username 영역 -->
                <div class="metadata">
                    <div class="date">
                        October 10, 2020
                    </div>
                    <div class="username">
                        anonymous
                    </div>
                </div>
                <!-- contents 조회/수정 영역-->
                <div class="contents">
                    <div id="1-contents" class="text">
                        dsafnkalfklewakflekelafkleajfkleafkldsankflenwaklfnekwlafneklwanfkelawnfkelanfkleanfklew
                    </div>
                    <div id="1-editarea" class="edit">
                        <textarea id="1-textarea" class="te-edit" name="" id="" cols="30" rows="5"></textarea>
                    </div>
                </div>
                <!-- 버튼 영역-->
                <div class="footer">
                    <img id="1-edit" onclick="editPost('1')" class="icon-start-edit" src="edit.png" alt="">
                    <img id="1-delete" onclick="deleteOne('1')" class="icon-delete" src="delete.png" alt="">
                    <img id="1-submit" onclick="submitEdit('1')" class="icon-end-edit" src="done.png" alt="">
                </div>
            </div>
        </div>
    </div>
    </body>
    
    </html>
    

17. [3주차] 끝 & 숙제설명

  • 잠깐 복습

    드디어! 우리가 End to End 로, 처음부터 끝까지, 를 만들어 보았습니다.🎉 다소 빠르고 힘겹게 느껴지신다구요? 제대로 공부하고 계시다는 증거입니다. 😎

    약간의 속도감이 우리의 실력을 한 차원 끌어올리고 있어요! 4주차 수업 <나만의 셀렉샵="">을 위해, 이번 주 잘 소화해주실거죠?

    • Rest API의 CRUD 전체 프로세스를, 다시 한 번 복습했습니다.
    • HTML, CSS, Javascript, jQuery를 통해 보이는 화면을 만들고 조작하는 방법에 대해 배웠습니다.
    • API를 먼저 설계하고, 그에 따라 클라이언트 코드 작성하는 프로세스를 익혔습니다.
  • 숙제 설명

    타임라인 서비스가 불러오는 메모 목록의 시간을, 조회 시간으로부터 24시간 이내로 바꿔보세요. 힌트1) spring jpa localtime between 라고 구글링해보세요. 힌트2) 지금은 LocalDateTime.now(), 하루 전은 LocalDateTime.now().minusDays(1) 입니다.

    • 제출 파일
      • MemoRepository.java
      • MemoController.java





© 2021.03. by yacho

Powered by github