[TypeScript] Drag&Drop 프로젝트 만들기

HIHI JIN·2023년 3월 19일
0

typescript

목록 보기
7/10
post-thumbnail

타입스크립트 핸드북Udemy강의를 토대로 Typescript를 공부합니다.

//ts를 시작할 때
npm install
npm install -g typescript
npm install --save-dev lite-server
npm start
tsc -w (관찰모드)
tsc(렌더링)

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>ProjectManager</title>
    <link rel="stylesheet" href="app.css" />
    <script src="dist/app.js" defer></script>
  </head>
  <body>
    <template id="project-input">
      <form>
        <div class="form-control">
          <label for="title">Title</label>
          <input type="text" id="title" />
        </div>
        <div class="form-control">
          <label for="description">Description</label>
          <textarea id="description" rows="3"></textarea>
        </div>
        <div class="form-control">
          <label for="people">People</label>
          <input type="number" id="people" step="1" min="0" max="10" />
        </div>
        <button type="submit">ADD PROJECT</button>
      </form>
    </template>
    <template id="single-project">
      <li>
      </li>
    </template>
    <template id="project-list">
      <section class="projects">
        <header>
          <h2></h2>
        </header>
        <ul></ul>
      </section>
    </template>
    <div id="app"></div>
  </body>
</html>

app.css

* {
  box-sizing: border-box;
}

html {
  font-family: sans-serif;
}

body {
  margin: 0;
}

label,
input,
textarea {
  display: block;
  margin: 0.5rem 0;
}

label {
  font-weight: bold;
}

input,
textarea {
  font: inherit;
  padding: 0.2rem 0.4rem;
  width: 100%;
  max-width: 30rem;
  border: 1px solid #ccc;
}

input:focus,
textarea:focus {
  outline: none;
  background: #fff5f9;
}

button {
  font: inherit;
  background: #ff0062;
  border: 1px solid #ff0062;
  cursor: pointer;
  color: white;
  padding: 0.75rem 1rem;
}

button:focus {
  outline: none;
}

button:hover,
button:active {
  background: #a80041;
  border-color: #a80041;
}

.projects {
  margin: 1rem;
  border: 1px solid #ff0062;
}

.projects header {
  background: #ff0062;
  height: 3.5rem;
  display: flex;
  justify-content: center;
  align-items: center;
}

#finished-projects {
  border-color: #0044ff;
}

#finished-projects header {
  background: #0044ff;
}

.projects h2 {
  margin: 0;
  color: white;
}

.projects ul {
  list-style: none;
  margin: 0;
  padding: 1rem;
}

.projects li {
  box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.26);
  padding: 1rem;
  margin: 1rem;
  background-color: white;
}

.projects li h2 {
  color: #ff0062;
  margin: 0.5rem 0;
}

#finished-projects li h2 {
  color: #0044ff;
}

.projects li h3 {
  color: #575757;
  font-size: 1rem;
}

.project li p {
  margin: 0;
}

.droppable {
  background: #ffe3ee;
}

#finished-projects .droppable {
  background: #d6e1ff;
}

#user-input {
  margin: 1rem;
  padding: 1rem;
  border: 1px solid #ff0062;
  background: #f7f7f7;
}

DOM 요소 선택 및 OOP렌더링

class projectInput 만들고 constructor 에서 html요소들을 불러오는 단계
private attach 메소드를 만들어 렌더링이 될 수 있게 만드는 단계

//템플릿과 렌더링되는 위치에 접근
class ProjectInput {
    templateElement: HTMLTemplateElement;//html타입으로 설정
    //templeteElement는 null은 저장하지 않는다.

    //hostElement는 html에서 div태그역할을 해야 한다.
    hostElement: HTMLDivElement;

    //템플릿 엘리먼트의 첫번째 태그가 Form태그
    element: HTMLFormElement;
    constructor(){
        //templateElement는 이 내용을 보유하는 템플릿에 대한 접근성을 제공
        //project-input은 template의 아이디!
        //1번째 오류 : 타입스크립트는 html을 읽지 않으므로 이 id엘리먼트는 null값이 된다.
        //이 엘리먼트의 값이 null이 아니라고 해야된다. 방법은 !(느낌표)
        //2번째 오류 :  content 프로퍼티가 'HTMLElement'에서 누락됨
        //타입스크립트에 우리는 확실히 알고 있는 타입을 말해줘야 한다. ->타입 캐스팅(type casting)을 이용
        //앞에 <HTMLTemplateElement> 하거나 뒤에 as HTMLTemplateElement라고 타입캐스팅 하면 된다.
        this.templateElement = document.getElementById("project-input")! as HTMLTemplateElement;//null이 아니고 html타입이 될거라는 보장을 한다!

        //hostElement는 템플릿 내용을 렌더링하려는 엘리먼트에 대한 참조를 보유한다.
        //"app"은 hostElement가 될 div의 아이디
        //hostElement도 templateElement와 똑같이 타입캐스팅 필요!
        this.hostElement = document.getElementById("app")! as HTMLDivElement;
        //이제 이 엘리먼트들에 접근할 수는 있지만 아직 아무것도 렌더링하지는 못한다.
        //템플릿 태그 안에 있는 것을 가져와서 이것을 dom에 렌더링해야 한다!
        
        //이 클래스의 새로운 인스턴스를 생성한다면 이 인스턴스에 속하는 양식을 곧바로 렌더링 할 수 있다.
        //전역문서객체에 제공되는 메서드, 메서드 전달인자에 템플릿 엘리먼트에 포인터를 넣는다.
        //content는 HTMLTemplateElement에 존재하는 프로퍼티이고 단순히 템플릿의 내용을 참조한다.
        //템플릿 코드 사이의 html 코드를 참조한다. importedNode는 모두 두 번째 인자를 취한다.
        //이 인자는 Deep Clone를 이용하여 가져오기를 할 것인지 말 것인지,
        //그래서 템플릿 내부에 있는 모든 수준으로 가져올 것인지를 정의한다. 원한다면 두번째 인자에 true 넣기
        const importedNode = document.importNode(this.templateElement.content, true);
        //이제 importedNode의 타입은 DocumentFragment 라고 추론한다.
    
        this.element = importedNode.firstElementChild as HTMLFormElement;
    
        this.attach(); //메서드 호출
    }   
    private attach(){
        //콘텐츠를 렌더링하고 싶은 위치에 insert~메서드를 사용
        //이것은 HTMLElement를 삽입하기 위해 자바스크립트 브라우저에서 제공하는 기본 메서드
        this.hostElement.insertAdjacentElement('afterbegin', this.element);
    }
}

const PrjInput = new ProjectInput();
console.log(PrjInput);

첫번째 양식 form을 렌더링했는데, 스타일을 아직 건들지 않았음.

DOM 요소와 상호 작용

private configure 메소드로 'submit' 이벤트를 생성하는 단계
private submitHandler를 만드는 단계

class ProjectInput {
    templateElement: HTMLTemplateElement;
    hostElement: HTMLDivElement;
    element: HTMLFormElement;
    
    //form입력값을 가지고 오기 위한 프로퍼티
    //form이 제출되었을 때 값을 읽을 수 있게 한다.
    titleInputElement: HTMLInputElement;
    descriptionInputElement: HTMLInputElement;
    peopleInputElement: HTMLInputElement;

    constructor(){
        this.templateElement = document.getElementById("project-input")! as HTMLTemplateElement;

        this.hostElement = document.getElementById("app")! as HTMLDivElement;

        const importedNode = document.importNode(this.templateElement.content, true);
    
        this.element = importedNode.firstElementChild as HTMLFormElement;
    
        this.element.id = 'user-input';//렌더링된 엘리먼트에 ID가 있다고 접근한다. 그러면 css에서 #user-input의 스타일 속성을 적용할 수 있다.

        //this.titleInputElement = this.element.querySelector("#title"); //htmlformelement에서 title이라는 id을 가진 요소를 titleinputelement로 하겠다.
        //타입스크립트는 querySelector가 여기에 inputElement를 반환할 것이라는 것을 알 수가 없디.->타입캐스팅 이용
        this.titleInputElement = this.element.querySelector("#title") as HTMLInputElement;
        this.descriptionInputElement = this.element.querySelector("#description") as HTMLInputElement;
        this.peopleInputElement = this.element.querySelector("#people") as HTMLInputElement;

        //두 메서드는 private이므로 클래스 내부에서만 접근 가능하다.
        this.configure();
        this.attach();
    }   
    private submitHandler(event: Event){
        event.preventDefault(); //form 제출 기본 동작 방지
        console.log(this.titleInputElement.value); //저장되지 않은 프로퍼티의 value를 읽을 수 없다는 에러!
        //문제 : this 키워드가 submitHandler에서 저 클래스를 가리키지 않는다는 점
        //해결책 : submitHandle에서 bind 호출(이벤트리스너가 실행될때 실행방식을 미리 구성)
        //첫번째 인자 : this 키워드가 참조할 대상
        //configure메서드는 위에서 this키워드로 호출했다. 이때 this는 클래스! 이 this를 참조한다는 뜻이다.
        //이제 titleInputElement.value가 콘솔에 잘찍힌다.
    }
    //form이 제출되었을 때 값을 읽을 수 있게 한다.
    //이벤트리스너로 제출을 설정, 사용자입력의 유효성 검사하기
    private configure(){
        this.element.addEventListener('submit', this.submitHandler.bind(this));//submitHandler메서드를 이벤트리스너에 바인딩
    }
    private attach(){
        this.hostElement.insertAdjacentElement('afterbegin', this.element);
    }
}

const PrjInput = new ProjectInput();
console.log(PrjInput);

Autobind" 데코레이터 생성 및 사용하기

autobind데코레이터를 만들어 자동 바인딩되게 하는 단계

//autobind decorator
//바인딩할 메서드 이름과 그메서드의 descriptor(함수를 보유한 프로퍼티)를 인자로 받는다.
//~~원래의 프로퍼티 : (target: any, methodName: string, descriptor: PropertyDescriptor
function autobind(_: any, _2: string, descriptor: PropertyDescriptor){
    //~~오류:'methodName'이 선언되었지만 값이 읽히지 않는다 
    //해결책1 :tsconfig로 가서 엄격한 규칙을 완화
    //"noUnusedParameters": true -> false로 바꾸기(사용하지 않는 변수를 허용한다.) 
    //해결책2(선택) : 프로퍼티이름을 _(밑줄)로 하는 것. ts와 js에 이 값을 사용하지 않을 것을 알지만 나중에 인자가 필요하기 때문에 이 값을 수용해야 한다는 암시를 준다.

    //원래 메서드에 접근
    const originalMethod = descriptor.value;

    //조정된 descriptor 생성
    const adjDescriptor: PropertyDescriptor = {
        configurable: true, //언제든 수정가능하게 만든다.
        get(){  //게터 내부에서는 함수에 접근할 때 실행되어야 하는 bound함수 설정
            const boundFn = originalMethod.bind(this); //원래메서드에서 bind(this)호출한 bound함수 리턴
            return boundFn;
        }
    };
    return adjDescriptor; //전체적으로는 해당 메서드 데코레이터에서 조정된 descriptor를 반환한다.
}

//ProjectInput Class
class ProjectInput {
    templateElement: HTMLTemplateElement;
    hostElement: HTMLDivElement;
    element: HTMLFormElement;
    
    titleInputElement: HTMLInputElement;
    descriptionInputElement: HTMLInputElement;
    peopleInputElement: HTMLInputElement;

    constructor(){
        this.templateElement = document.getElementById("project-input")! as HTMLTemplateElement;

        this.hostElement = document.getElementById("app")! as HTMLDivElement;

        const importedNode = document.importNode(this.templateElement.content, true);
    
        this.element = importedNode.firstElementChild as HTMLFormElement;
    
        this.element.id = 'user-input';

        this.titleInputElement = this.element.querySelector("#title") as HTMLInputElement;
        this.descriptionInputElement = this.element.querySelector("#description") as HTMLInputElement;
        this.peopleInputElement = this.element.querySelector("#people") as HTMLInputElement;

        this.configure();
        this.attach();
    }   

    @autobind //바인딩을 위해 데코레이터를 사용
    private submitHandler(event: Event){ //~~Experimental support for decorators is a feature...'라는 오류 : tsconfig 파일-> "experimentalDecorators": true,로 고치기!
        event.preventDefault();
        console.log(this.titleInputElement.value);
    }
    private configure(){
        //this.element.addEventListener('submit', this.submitHandler.bind(this));
        //앱 여러 곳에서 bind를 호출해야하는 경우, 쉽게 추가할 수 있는 데코레이터 이용해보기
        this.element.addEventListener('submit', this.submitHandler);
    }
    private attach(){
        this.hostElement.insertAdjacentElement('afterbegin', this.element);
    }
}

const PrjInput = new ProjectInput();
console.log(PrjInput);

사용자 입력 가져오기

private gatherUserInput 메소드를 만들어 밸류 값들을 받아오는 단계

//autobind decorator
function autobind(_: any, _2: string, descriptor: PropertyDescriptor){
    const originalMethod = descriptor.value;

    const adjDescriptor: PropertyDescriptor = {
        configurable: true, 
        get(){
            const boundFn = originalMethod.bind(this);
            return boundFn;
        }
    };
    return adjDescriptor;
}

//ProjectInput Class
class ProjectInput {
    templateElement: HTMLTemplateElement;
    hostElement: HTMLDivElement;
    element: HTMLFormElement;
    
    titleInputElement: HTMLInputElement;
    descriptionInputElement: HTMLInputElement;
    peopleInputElement: HTMLInputElement;

    constructor(){
        this.templateElement = document.getElementById("project-input")! as HTMLTemplateElement;

        this.hostElement = document.getElementById("app")! as HTMLDivElement;

        const importedNode = document.importNode(this.templateElement.content, true);
    
        this.element = importedNode.firstElementChild as HTMLFormElement;
    
        this.element.id = 'user-input';

        this.titleInputElement = this.element.querySelector("#title") as HTMLInputElement;
        this.descriptionInputElement = this.element.querySelector("#description") as HTMLInputElement;
        this.peopleInputElement = this.element.querySelector("#people") as HTMLInputElement;

        this.configure();
        this.attach();
    }
    //반환타입은 튜플:구성요소의 수와 타입이 고정되는 배열
    //입력값이 없으면 리턴값이 없을 수 있으므로 void도 반환 타입으로 둔다.
    private gatherUserInput(): [string, string, number] | void{
        const enteredTitle = this.titleInputElement.value;
        const enteredDescription = this.descriptionInputElement.value;
        const enteredPeople = this.peopleInputElement.value;

        //title, description, people입력을 확인하는 검증
        //trim()메서드는 공백제거 메서드
        if(enteredTitle.trim().length===0 || enteredDescription.trim().length===0 || enteredPeople.trim().length===0){
            alert("Invalid input, please try again!");
            return;
        }else{
            return [enteredTitle, enteredDescription, +enteredPeople];
        }
    }
    //submit후 입력값이 삭제되는 메서드
    private clearInputs(){
        this.titleInputElement.value = '';
        this.descriptionInputElement.value = '';
        this.peopleInputElement.value = '';
    }

    @autobind 
    private submitHandler(event: Event){
        event.preventDefault();
        //console.log(this.titleInputElement.value);
        const userInput = this.gatherUserInput();
        //모든 입력값에 도달하고 사용자 입력을 취합하며 검증되고 반환하면 여기서 자신의 사용자 입력값을 얻을 수 있다.

        //런타임 중 사용자입력값이 튜플인지 확인 -> js는 튜플을 확인할 수 없으므로 배열인지 확인한다.
        if(Array.isArray(userInput)){
            const [title, desc, people] = userInput;
            console.log(title,desc, people); //콘솔에 잘찍힌다. 입력값이 없으므로 alert가 작동한다.
            this.clearInputs(); //콘솔에 출력된 후 form은 삭제된다.
        }
    }
    private configure(){
        this.element.addEventListener('submit', this.submitHandler);
    }
    private attach(){
        this.hostElement.insertAdjacentElement('afterbegin', this.element);
    }
}

const PrjInput = new ProjectInput();
console.log(PrjInput);

재사용 가능한 검증 기능 생성

사용자입력 값을 받아올때 유효성 검사를 위해 인터페이스를 만드는 단계

//Validation 검증가능한 인터페이스 생성
interface validatable{
    value: string|number;
    required?: boolean;
    minLength?: number; //문자열의 길이 확인
    maxLength?: number; //문자열의 길이 확인
    min?: number; //수치값이 특정 최소값 이상인지 확인
    max?: number; //수치값이 특정 최대값 이하인지 확인
} //value외에 모두 선택사항이어야 하므로 명칭 뒤에 물음표를 사용하여 정의되지 않은 값을 허용한다.

//위 인터페이스로 검증 함수 생성(검증함수는 검증 객체를 취해야 한다)
function validate(validatableInput: validatable){
    let isValid = true; //초기 참인 유효한 변수 생성
    //디폴트는 true, 확인 중 하나라도 실패하면 false
    if(validatableInput.required){
        isValid = isValid && validatableInput.value.toString().trim().length!==0;
        //&& 뒤에 온 것이 false이면 isValid 새값도 false로 검증
        //둘중 하나가 false면 전체값 false
        //validatableInput.value이 비어있는지 확인
        //value가 숫자일지도 모르므로 아예string타입으로 변환
        //결론: 공란이 아니면 유효한값이라고 true를 반환
    }
    //특정 최소 길이 조건에 맞는 문자열인가?검증
    //value타입이 string일 경우 길이를 정의할 수 있으므로 타입가드를 추가한다.
    //최소길이가 0으로 설정된경우, 뛰어넘지말고 최소길이가 0인지 확인하라는 조건을 걸기 위해 null이 아닐경우를 추가한다.
    if(validatableInput.minLength !== null &&
        typeof validatableInput.value==="string"){
        isValid = isValid && validatableInput.value.length > validatableInput.minLength;
        //value의 길이가 최소길이조건보다 커야 true
    }
    if(validatableInput.maxLength !== null &&
        typeof validatableInput.value==="string"){
        isValid = isValid && validatableInput.value.length < validatableInput.maxLength;
        //value의 길이가 최대길이조건보다 커야 true
    }
    //min이 0일 경우 false로 지나갈수도 있으므로 null이 아닐경우조건을 추가해서 0인경우도 검사하게 한다.
    //value가 number타입일 경우만 검사한다.
    if(validatableInput.min !== null && typeof validatableInput.value==="number"){
        isValid = isValid && validatableInput.value > validatableInput.min;
    }
    if(validatableInput.max !== null && typeof validatableInput.value==="number"){
        isValid = isValid && validatableInput.value < validatableInput.max;
    }
    return isValid;
}

//autobind decorator
function autobind(_: any, _2: string, descriptor: PropertyDescriptor){
    const originalMethod = descriptor.value;

    const adjDescriptor: PropertyDescriptor = {
        configurable: true, 
        get(){
            const boundFn = originalMethod.bind(this);
            return boundFn;
        }
    };
    return adjDescriptor;
}


//ProjectInput Class
class ProjectInput {
    templateElement: HTMLTemplateElement;
    hostElement: HTMLDivElement;
    element: HTMLFormElement;
    
    titleInputElement: HTMLInputElement;
    descriptionInputElement: HTMLInputElement;
    peopleInputElement: HTMLInputElement;

    constructor(){
        this.templateElement = document.getElementById("project-input")! as HTMLTemplateElement;

        this.hostElement = document.getElementById("app")! as HTMLDivElement;

        const importedNode = document.importNode(this.templateElement.content, true);
    
        this.element = importedNode.firstElementChild as HTMLFormElement;
    
        this.element.id = 'user-input';

        this.titleInputElement = this.element.querySelector("#title") as HTMLInputElement;
        this.descriptionInputElement = this.element.querySelector("#description") as HTMLInputElement;
        this.peopleInputElement = this.element.querySelector("#people") as HTMLInputElement;

        this.configure();
        this.attach();
    }
    //입력값의 유효성 검사, 유효하면 입력값을 튜플형태로 반환
    private gatherUserInput(): [string, string, number] | void{
        const enteredTitle = this.titleInputElement.value;
        const enteredDescription = this.descriptionInputElement.value;
        const enteredPeople = this.peopleInputElement.value;

        //재사용가능한 검증 기능 생성
        //원래 코드 : if(enteredTitle.trim().length===0 || enteredDescription.trim().length===0 || enteredPeople.trim().length===0)
        //validate함수의 검증이 유효한 경우 true, 아니면 false
        //각각의 유효성이 true라면 실행, 하나라도 false라면 alert동작
        if(
            validate({value: enteredTitle, required:true, minLength: 5}) &&
            validate({value: enteredDescription, required:true, minLength: 5}) &&
            validate({value: enteredPeople, required:true, minLength: 5})
            ){
            alert("Invalid input, please try again!");
            return;
        }else{
            return [enteredTitle, enteredDescription, +enteredPeople];
        }
    }
    //submit 클릭후 입력값이 삭제되는 메서드
    private clearInputs(){
        this.titleInputElement.value = '';
        this.descriptionInputElement.value = '';
        this.peopleInputElement.value = '';
    }

    //데코레이터
    @autobind 
    //submit버튼 클릭시 실행되는 메서드
    private submitHandler(event: Event){
        event.preventDefault();
        const userInput = this.gatherUserInput();

        if(Array.isArray(userInput)){
            const [title, desc, people] = userInput;
            console.log(title,desc, people);
            this.clearInputs(); 
        }
    }
    //submit이벤트를 생성하는 메서드
    private configure(){
        this.element.addEventListener('submit', this.submitHandler);
    }
    //form element를 렌더링하는 메서드
    private attach(){
        this.hostElement.insertAdjacentElement('afterbegin', this.element);
    }
}

const PrjInput = new ProjectInput();
console.log(PrjInput);
//다른 방법
//입력값의 유효성 검사, 유효하면 입력값을 튜플형태로 반환
    private gatherUserInput(): [string, string, number] | void{
        const enteredTitle = this.titleInputElement.value;
        const enteredDescription = this.descriptionInputElement.value;
        const enteredPeople = this.peopleInputElement.value;

        const titleValidatable: validatable = {
            value: enteredTitle,
            required: true
        }
        const descriptionValidatable: validatable = {
            value: enteredDescription,
            required: true,
            minLength: 5
        }
        const peopleValidatable: validatable = {
            value: +enteredPeople,
            required: true,
            min: 1,
            max: 5
        }

        if(
            !validate(titleValidatable) ||
            !validate(descriptionValidatable) ||
            !validate(peopleValidatable)
            ){
            alert("Invalid input, please try again!");
            return;
        }else{
            return [enteredTitle, enteredDescription, +enteredPeople];
        }
}

렌더링 프로젝트 목록

project list를 active와 finished로 나누어 화면에 렌더링하는 단계

interface validatable{...} 

function validate(validatableInput: validatable){...}

function autobind(_: any, _2: string, descriptor: PropertyDescriptor){...}

//프로젝트 목록을 생성하는 새로운 클래스 생성
//ProjectInput Class에서 취합된 입력을 기본적으로 projectlist로 넘기고 여기에 새로운 항목을 추가할 방법을 찾는다.
// ProjectInput class와 같이 html의 템플릿에 도달해서 앱의 특정 위치에 생성한다.
class ProjectList{
    templateElement: HTMLTemplateElement;
    hostElement: HTMLDivElement;
    element: HTMLElement;
    //HTMLSectionElement를 타입으로 설정해야 하지만, 타입스크립트에서 없다고 인식하므로 특화된 유형을 제외하면 일반적으로 HTMLElement라고 설정해도 된다.

    constructor(private type: 'active'|'finished'){
        this.templateElement = document.getElementById("project-list")! as HTMLTemplateElement;

        this.hostElement = document.getElementById("app")! as HTMLDivElement;

        const importedNode = document.importNode(this.templateElement.content, true);
    
        this.element = importedNode.firstElementChild as HTMLElement; //내용을 불러들인 템플릿의 첫번째 구성요소를 저장한다.
    
        this.element.id = `${type}-projects`;
        //id는 데이터 변경 불가하게 기록되면 안되고 동적이어야 한다.
        //왜냐면 최종 앱에 두개의 목록을 가지려고 한다.
        //하나는 활성프로젝트용, 다른 하나는 비활성 프로젝트용
        //생성자 함수에서 매개변수를 유니언타입으로 활성, 비활성 타입을 가지게 한다.
        //결과적으로 id는'active-projects' 또는 'finished-projects'를 가진다.
        this.attach();
        this.renderContent();
    }
    //projectlist의 h2요소에 이벤트리스너 추가
    //프/projectlist의 일부인 불규칙 목록에 ID를 추가
    private renderContent(){
        const listId = `${this.type}-projects-list`; //주변 projectlist의 type에 기반하고 project-list를 추가한다.
        
        //이제 불규칙목록에 접근할 수 있다.
        //!(느낌표) : 찾을 수 있다는 것을 알고 있으므로 무효 경우를 배제한다.
        this.element.querySelector("ul")!.id = listId; //ul요소 아이디 추가
        this.element.querySelector("h2")!.textContent = this.type.toUpperCase() + 'PROJECTS'; //ACTIVE PROJECTS or FINISHED PROJECTS
    }

    private attach(){ //this.element 템플릿에서 추출하는 메서드(beforeend : host element의 종료 태크 앞)
        this.hostElement.insertAdjacentElement('beforeend', this.element);
    }
}



//ProjectInput Class : form생성과 사용자입력값 수집을 담당
class ProjectInput {
    templateElement: HTMLTemplateElement;
    hostElement: HTMLDivElement;
    element: HTMLFormElement;
    
    titleInputElement: HTMLInputElement;
    descriptionInputElement: HTMLInputElement;
    peopleInputElement: HTMLInputElement;

    constructor(){
        this.templateElement = document.getElementById("project-input")! as HTMLTemplateElement;

        this.hostElement = document.getElementById("app")! as HTMLDivElement;

        const importedNode = document.importNode(this.templateElement.content, true);
    
        this.element = importedNode.firstElementChild as HTMLFormElement;
    
        this.element.id = 'user-input';

        this.titleInputElement = this.element.querySelector("#title") as HTMLInputElement;
        this.descriptionInputElement = this.element.querySelector("#description") as HTMLInputElement;
        this.peopleInputElement = this.element.querySelector("#people") as HTMLInputElement;

        this.configure();
        this.attach();
    }
    //입력값의 유효성 검사, 유효하면 입력값을 튜플형태로 반환
    private gatherUserInput(): [string, string, number] | void{
        const enteredTitle = this.titleInputElement.value;
        const enteredDescription = this.descriptionInputElement.value;
        const enteredPeople = this.peopleInputElement.value;

        const titleValidatable: validatable = {
            value: enteredTitle,
            required: true
        }
        const descriptionValidatable: validatable = {
            value: enteredDescription,
            required: true,
            minLength: 5
        }
        const peopleValidatable: validatable = {
            value: +enteredPeople,
            required: true,
            min: 1,
            max: 5
        }

        if(
            !validate(titleValidatable) || !validate(descriptionValidatable) || !validate(peopleValidatable)){
            alert("Invalid input, please try again!");
            return;
        }else{
            return [enteredTitle, enteredDescription, +enteredPeople];
        }
    }
    //submit 클릭후 입력값이 삭제되는 메서드
    private clearInputs(){
        this.titleInputElement.value = '';
        this.descriptionInputElement.value = '';
        this.peopleInputElement.value = '';
    }

    //데코레이터
    @autobind 
    //submit버튼 클릭시 실행되는 메서드
    private submitHandler(event: Event){
        event.preventDefault();
        const userInput = this.gatherUserInput();

        if(Array.isArray(userInput)){
            const [title, desc, people] = userInput;
            console.log(title,desc, people);
            this.clearInputs(); 
        }
    }
    //submit이벤트를 생성하는 메서드
    private configure(){
        this.element.addEventListener('submit', this.submitHandler);
    }
    //form element를 렌더링하는 메서드
    private attach(){
        this.hostElement.insertAdjacentElement('afterbegin', this.element);
    }
}

const PrjInput = new ProjectInput(); //프로젝트입력값 객체를 콘솔에 찍었다.
const activePrjList = new ProjectList('active');
const finishedPrjList = new ProjectList('finished');

싱글톤으로 애플리케이션 상태 관리하기

addproject버튼 클릭 시 사용자 입력값 중 title이 각각 프로젝트 목록으로 렌더링되게 만드는 단계

//앱 상태를 관리하는 클래스
//프로젝트와 앱관리 대상이 되는 상태를 관리, 앱과 관련된 각기 다른 부분의 이벤트리스너를 설정할 수 있다.
//전역상태관리 객체가 있으면, 변경사항만 업데이트하면 된다.
//project state management class
class ProjectState{
    //새 프로젝트의 해당정보를 프로젝트목록클래스에 넘기면 해당 클래스가 화면출력을 할 수 있다.
    //요소로 함수가 들어가므로, 함수 참조 배열이다.
    //새로운 프로젝트를 추가할 때 변화가 있을 때마다 모든 리스너 함수를 소환하는 개념이다.
    private listeners: any[] = []; 

    private projects: any[] = [];

    //싱글톤 클래스임을 확실히 하고자할 때 코드
    private static instance: ProjectState;
    private constructor(){}

    //ProjectState 클래스 타입인 instance가 있다면 그대로 리턴 하고, 없다면 새로운 projectState를 생성한다.
    static getInstance(){
        if(this.instance) return this.instance;
        this.instance = new ProjectState();
        return this.instance;
    }

    addListner(listenerFn: Function){
        this.listeners.push(listenerFn); //함수인 매개변수를 listener속성에 넣는다.
    }

    //add project버튼을 클릭할 때마다 해당목록에 항목을 추가하려고 한다.
    addProject(title: string, description: string, numOfPeople: number){
        const newProject = {
            id: Math.random().toString,
            title: title,
            description: description,
            people: numOfPeople
        };
        this.projects.push(newProject);
        for(const listenerFn of this.listeners){ //리스너배열에 있는 요소인 함수들을 차례로 꺼내서 실행한다.
            listenerFn(this.projects.slice()); //프로젝트를 복사해서 리스너 함수를 실행시킨다.
        }
    }
}
//문제 : 사용자 입력을 취합한 클래스, submitHandler 안에서 addProject를 어떻게 호출할 것인가
//그리고 바뀔 때마다 해당 업데이트된 프로젝트 목록을 어떻게 프로젝트 목록 클래스에 전달할 것인가
//projectState객체를 전역 상수를 만들어 파일 어디에서나 사용될 수 있게 한다.
//const projectState = new projectState();
const projectState = ProjectState.getInstance(); //projectState 새로운 객체 생성

//Validation 검증가능한 인터페이스 생성
interface validatable{...} 

//위 인터페이스로 검증 함수 생성(검증함수는 검증 객체를 취해야 한다)
function validate(validatableInput: validatable){
    let isValid = true; 

    if(validatableInput.required){...}

//autobind decorator
function autobind(_: any, _2: string, descriptor: PropertyDescriptor){...}
    
//ProjectList Class : active/finished projectlist를 생성하는 클래스
class ProjectList{
    templateElement: HTMLTemplateElement;
    hostElement: HTMLDivElement;
    element: HTMLElement;
    assignedProjects: any[]; //변경된 프로젝트를 얻을 배열

    constructor(private type: 'active'|'finished'){
        this.templateElement = document.getElementById("project-list")! as HTMLTemplateElement;

        this.hostElement = document.getElementById("app")! as HTMLDivElement;

        this.assignedProjects = [] //초기화

        const importedNode = document.importNode(this.templateElement.content, true);
    
        this.element = importedNode.firstElementChild as HTMLElement;
    
        this.element.id = `${this.type}-projects`;


        //리스너 배열의 함수를 차례대로 실행하는 메서드
        projectState.addListner((projects: any[])=>{
            this.assignedProjects = projects;  //assgiendProjects를 새로운 프로젝트로 재정의
            this.renderProjects();
        });


        this.attach();
        this.renderContent();
    }
    //assignedProjects을 새로운 프로젝트로 재정의하는 메서드
    private renderProjects(){ //HTMLUListElement:불규칙목록 구성요소
    const listEl = document.getElementById(`${this.type}-projects-list`)! as HTMLUListElement//아래의 할당한 id를 가지고 목록을 찾는다.
    //이 목록에 보유한 모든 프로젝트를 렌더링하고자 한다.
    //this.assignedProjects의 모든 프로젝트 항목을 살펴보고 모든 항목에 대해 목록에 추가하고 추후에 정렬한다.
    for(const prjItem of this.assignedProjects){
        const listItem = document.createElement('li');
        listItem.textContent = prjItem.title; //li요소의 text는 item의 title
        listEl.appendChild(listItem); //li요소를 ul요소에 append
    }
    }


    private renderContent(){
        const listId = `${this.type}-projects-list`; //프로젝트 생성이 끝나면 해당 id를 할당한다.
        
        this.element.querySelector("ul")!.id = listId; 
        this.element.querySelector("h2")!.textContent = this.type.toUpperCase() + 'PROJECTS'; 
    }

    private attach(){ 
        this.hostElement.insertAdjacentElement('beforeend', this.element);
    }
}



//ProjectInput Class : form생성과 사용자입력값 수집을 담당
class ProjectInput {
    templateElement: HTMLTemplateElement;
    hostElement: HTMLDivElement;
    element: HTMLFormElement;
    
    titleInputElement: HTMLInputElement;
    descriptionInputElement: HTMLInputElement;
    peopleInputElement: HTMLInputElement;

    constructor(){
        this.templateElement = document.getElementById("project-input")! as HTMLTemplateElement;

        this.hostElement = document.getElementById("app")! as HTMLDivElement;

        const importedNode = document.importNode(this.templateElement.content, true);
    
        this.element = importedNode.firstElementChild as HTMLFormElement;
    
        this.element.id = 'user-input';

        this.titleInputElement = this.element.querySelector("#title") as HTMLInputElement;
        this.descriptionInputElement = this.element.querySelector("#description") as HTMLInputElement;
        this.peopleInputElement = this.element.querySelector("#people") as HTMLInputElement;

        this.configure();
        this.attach();
    }
    //입력값의 유효성 검사, 유효하면 입력값을 튜플형태로 반환
    private gatherUserInput(): [string, string, number] | void{
        const enteredTitle = this.titleInputElement.value;
        const enteredDescription = this.descriptionInputElement.value;
        const enteredPeople = this.peopleInputElement.value;

        const titleValidatable: validatable = {
            value: enteredTitle,
            required: true
        }
        const descriptionValidatable: validatable = {
            value: enteredDescription,
            required: true,
            minLength: 5
        }
        const peopleValidatable: validatable = {
            value: +enteredPeople,
            required: true,
            min: 1,
            max: 5
        }

        if(
            !validate(titleValidatable) || !validate(descriptionValidatable) || !validate(peopleValidatable)){
            alert("Invalid input, please try again!");
            return;
        }else{
            return [enteredTitle, enteredDescription, +enteredPeople];
        }
    }
    //submit 클릭후 입력값이 삭제되는 메서드
    private clearInputs(){
        this.titleInputElement.value = '';
        this.descriptionInputElement.value = '';
        this.peopleInputElement.value = '';
    }

    //데코레이터
    @autobind 
    //submit버튼 클릭시 실행되는 메서드
    private submitHandler(event: Event){
        event.preventDefault();
        const userInput = this.gatherUserInput();

        if(Array.isArray(userInput)){
            const [title, desc, people] = userInput;
            console.log(title,desc, people);
            projectState.addProject(title, desc, people);
            //위에서 생성한 projectState객체에서 해당 프로젝트목록에 프로젝트를 추가하는 메서드
            this.clearInputs(); 
        }
    }
    //submit이벤트를 생성하는 메서드
    private configure(){
        this.element.addEventListener('submit', this.submitHandler);
    }
    //form element를 렌더링하는 메서드
    private attach(){
        this.hostElement.insertAdjacentElement('afterbegin', this.element);
    }
}

const PrjInput = new ProjectInput(); //프로젝트입력값 객체를 콘솔에 찍었다.
const activePrjList = new ProjectList('active'); //active project 객체
const finishedPrjList = new ProjectList('finished'); //finished project 객체


//현재 문제 : 첫번째는 정상, 두번째 구성요소를 추가할때부터 전의 마지막 구성요소를 복제하고 새로운 구성요소를 추가한다.
//그리고 active, finished 프로젝트목록 둘다에 들어간다.

더 많은 클래스 및 사용자 정의 타입

첫번째 add project클릭 시 정상 작동
두번째 add project클릭 시 앞의 항목이 같이 붙어서 또 나오는 문제 해결

//enum은 두개의 식별자 옵션을 취할 때 사용하기 좋다.
//class Project{constructor{public status: 'active'|'finished'}}하지 않아도 된다.
enum ProjectStatus {
    Active,
    Finished
}

//Project Type : 항상 동일한 구조를 갖는 프로젝트 객체 구축 방법을 취하는 클래스
//아래의 ProjectState 클래스의 addProject메서드에서 newProject객체의 키들을
//생성자의 매개변수에서 타입을 정의해준다.
//+더해서 projectstate에서 프로젝트 목록이 필터링되어야 하므로
//status키로 active or finished 타입을 정의한다.
class Project{
    constructor(
        public id: string, 
        public title: string, 
        public description: string, 
        public people: number, 
        public status: ProjectStatus){
    }
}

//새로운 커스텀 type listener 추가
//Listener은 listener함수의 배열
//반환타입은 무효, 리스너함수가 반환하는 값은 신경쓰지 않는다.
type Listener = (items: Project[]) => void;

//project state management class : 앱 상태를 관리하는 클래스, 전역상태관리 객체를 만든다.
class ProjectState{
    private listeners: Listener[] = []; //any[] -> Listenr타입을 이용한 배열

    private projects: Project[] = []; //any[] -> project클래스를 사용한 project배열

    //싱글톤 클래스임을 확실히 하고자할 때 코드
    private static instance: ProjectState;
    private constructor(){}

    static getInstance(){
        if(this.instance) return this.instance;
        this.instance = new ProjectState();
        return this.instance;
    }

    addListner(listenerFn: Listener){ //Function -> Listener함수임을 명확히 한다.
        this.listeners.push(listenerFn); 
    }

    //add project버튼을 클릭할 때마다 해당목록에 항목을 추가하려고 한다.
    addProject(title: string, description: string, numOfPeople: number){
        /*const newProject = {
            id: Math.random().toString,
            title: title,
            description: description,
            people: numOfPeople
        };*/
        //새로운 project 객체를 생성한 것은 원래의 객체와 동일하다.
        const newProject = new Project(
            Math.random().toString(), 
            title, 
            description, 
            numOfPeople,
            ProjectStatus.Active);
            //new Project가 디폴트로 active-project-list로 가길 바란다.
        this.projects.push(newProject);

        for(const listenerFn of this.listeners){ 
            listenerFn(this.projects.slice()); 
        }
    }
}
//projectState 새로운 객체 생성
const projectState = ProjectState.getInstance();

//Validation 검증가능한 인터페이스 생성
interface validatable{...}

//위 인터페이스로 검증 함수 생성(검증함수는 검증 객체를 취해야 한다)
function validate(validatableInput: validatable){...}

//autobind decorator
function autobind(_: any, _2: string, descriptor: PropertyDescriptor){...}

//ProjectList Class : active/finished projectlist를 생성하는 클래스
class ProjectList{
    templateElement: HTMLTemplateElement;
    hostElement: HTMLDivElement;
    element: HTMLElement;
    assignedProjects: Project[]; //any[]타입을 Project[]타입으로 이제 바꿀 수 있다.

    constructor(private type: 'active'|'finished'){
        this.templateElement = document.getElementById("project-list")! as HTMLTemplateElement;

        this.hostElement = document.getElementById("app")! as HTMLDivElement;

        this.assignedProjects = [] //초기화

        const importedNode = document.importNode(this.templateElement.content, true);
    
        this.element = importedNode.firstElementChild as HTMLElement;
    
        this.element.id = `${this.type}-projects`;

        //addListner를 사용한 곳에서 projects타입을 Project[]라고 설정해주었기 때문에 대상이 더욱 명확해졌다.
        projectState.addListner((projects: any[])=>{ //any[] -> Project[]
            this.assignedProjects = projects; 
            this.renderProjects();
        });


        this.attach();
        this.renderContent();
    }
    private renderProjects(){ 
    const listEl = document.getElementById(`${this.type}-projects-list`)! as HTMLUListElement;

    for(const prjItem of this.assignedProjects){
        const listItem = document.createElement('li');
        listItem.textContent = prjItem.title; 
        listEl.appendChild(listItem); 
    }
    }

    private renderContent(){
        const listId = `${this.type}-projects-list`;
        
        this.element.querySelector("ul")!.id = listId; 
        this.element.querySelector("h2")!.textContent = this.type.toUpperCase() + 'PROJECTS'; 
    }

    private attach(){ 
        this.hostElement.insertAdjacentElement('beforeend', this.element);
    }
}


//ProjectInput Class : form생성과 사용자입력값 수집을 담당
class ProjectInput {...}

const PrjInput = new ProjectInput(); //프로젝트입력값 객체를 콘솔에 찍었다.
const activePrjList = new ProjectList('active'); //active project 객체
const finishedPrjList = new ProjectList('finished'); //finished project 객체

열거형으로 프로젝트 필터링하기

active와 finished projectlist에서 대한 필터링 해결(addproject를 클릭하면 디폴트로 activeprojectlist로만 project가 들어가는 것까지 만드는 단계)

//enum은 두개의 식별자 옵션을 취할 때 사용
enum ProjectStatus {...}

//Project Class: 항상 동일한 구조를 갖는 프로젝트 객체 구축 방법을 취하는 클래스
class Project{...}

//새로운 커스텀 type listener 추가 : Listener은 listener함수의 배열
type Listener = (items: Project[]) => void;

//project state management class : 앱 상태를 관리하는 클래스, 전역상태관리 객체를 만든다.
class ProjectState{...}

//projectState 새로운 객체 생성
const projectState = ProjectState.getInstance();

//Validation 검증가능한 인터페이스 생성
interface validatable{...}

//autobind decorator
function autobind(...){...}

//ProjectList Class : active/finished projectlist를 생성하는 클래스
class ProjectList{
    templateElement: HTMLTemplateElement;
    hostElement: HTMLDivElement;
    element: HTMLElement;
    assignedProjects: Project[]; 

    constructor(private type: 'active'|'finished'){
        this.templateElement = document.getElementById("project-list")! as HTMLTemplateElement;

        this.hostElement = document.getElementById("app")! as HTMLDivElement;

        this.assignedProjects = []

        const importedNode = document.importNode(this.templateElement.content, true);
    
        this.element = importedNode.firstElementChild as HTMLElement;
    
        this.element.id = `${this.type}-projects`;

        //필터링의 최적의 장소는 리스너 함수!
        //프로젝트를 저장 및 생성하기 전에 먼저 프로젝트 유지 여부에 따라 필터링 한다.
        //ProjectStatus는 enum을 이용하여, active키를 가지고 있고 Project의 status는 active면 필터했을 때 담긴다.
        //ProjectStatus가 finished키를 가지고, project의 status가 finished면 필터할때 담긴다.
        //문제 : addProject클릭 시 active projects목록에만 담긴다. 첫번째 시도는 정상, 두번째 시도부터 전의 내용이 복제되어 나오는게 문제 -> 프로젝트 렌더링 방식과 관련
       
        projectState.addListner((projects: Project[])=>{
            const relevantProjects = projects.filter(prj => {
                if(this.type === 'active') return prj.status === ProjectStatus.Active;
                else return prj.status === ProjectStatus.Finished; 
            });
            this.assignedProjects = relevantProjects; //project -> relevantProjects 변경
            this.renderProjects();
        });


        this.attach();
        this.renderContent();
    }
    //모든 프로젝트 항목을 검토하고 목록에 붙이는 메서드
    //문제 : 화면에 프로젝트 항목이 이미 생성되있을 수도 있다.
    //해결 : 이미 생성된 것과 생성할 것을 비교하여 불필요한 재생성(리렌더링)을 피하는 것!
    //실제로 dom을 참조하여 비교작업을 하는 것은 번거로우니, 모든 목록항목을 없앤 후에 재생성하는 방법을 사용한다.
    private renderProjects(){ 
    const listEl = document.getElementById(`${this.type}-projects-list`)! as HTMLUListElement;
    listEl.innerHTML = ''; //새로운 프로젝트를 추가할 때마다 모든 프로젝트를 렌더링한다.

    for(const prjItem of this.assignedProjects){
        const listItem = document.createElement('li');
        listItem.textContent = prjItem.title; 
        listEl.appendChild(listItem); 
    }
    }

    private renderContent(){
        const listId = `${this.type}-projects-list`;
        
        this.element.querySelector("ul")!.id = listId; 
        this.element.querySelector("h2")!.textContent = this.type.toUpperCase() + 'PROJECTS'; 
    }

    private attach(){ 
        this.hostElement.insertAdjacentElement('beforeend', this.element);
    }
}
                       
                       
//ProjectInput Class : form생성과 사용자입력값 수집을 담당
class ProjectInput {...}

const PrjInput = new ProjectInput(); //프로젝트입력값 객체를 콘솔에 찍었다.
const activePrjList = new ProjectList('active'); //active project 객체
const finishedPrjList = new ProjectList('finished'); //finished project 객체

상속 & 제네릭 추가하기

상속과 기본 클래스를 사용하여 코드를 재사용가능하도록 개선하는 단계
Component 추상 클래스(베이스 클래스)를 만들어서 ProjectList클래스와 ProjectInput클래스에 상속하는 단계
애플리케이션이 커져서 복수 상태가 존재하는 ProjectState도 리팩토링

//enum은 두개의 식별자 옵션을 취할 때 사용
enum ProjectStatus {...}

//Project Class: 항상 동일한 구조를 갖는 프로젝트 객체 구축 방법을 취하는 클래스
class Project{...}

//새로운 커스텀 type listener 추가 : Listener은 listener함수의 배열
//리스너가 실제로 프로젝트 배열을 복귀시키는지는 알수 없음
    //-> 제네릭타입을 추가해서 외부와 구별한다.
type Listener<T> = (items: T[]) => void; //원래 코드 : type Listener = (items: Project[]) => void;

//state class : ProjectState의 두가지 상태를 옮긴다.
//제네릭 클래스 사용
class State<T>{
    //ProjectState의  리스너들은 비공개 상태이고 베이스 클래스에서 비공개이다.
    //이는 즉 베이스 클래스 내부에서만 접근 가능하다.
    //해결 : private와 비슷하지만 상속클래스에서의 접근은 허용하는 키워드를 사용한다.
    protected listeners: Listener<T>[] = []; //제네릭 타입 추가

    addListener(listenerFn: Listener<T>){ //제네릭 타입 추가
        this.listeners.push(listenerFn); 
    }
} 


//project state management class : 앱 상태를 관리하는 클래스, 전역상태관리 객체를 만든다.
//State 클래스를 상속받는다.
class ProjectState extends State<Project>{
    private projects: Project[] = []; 
    private static instance: ProjectState;

    //이제 비공개 생성자 사용
    private constructor(){
        super();
    }

    static getInstance(){
        if(this.instance) return this.instance;
        this.instance = new ProjectState();
        return this.instance;
    }
    addProject(title: string, description: string, numOfPeople: number){
        const newProject = new Project(
            Math.random().toString(), 
            title, 
            description, 
            numOfPeople,
            ProjectStatus.Active);

        this.projects.push(newProject);

        for(const listenerFn of this.listeners){ 
            listenerFn(this.projects.slice()); 
        }
    }
}
//projectState 새로운 객체 생성
const projectState = ProjectState.getInstance();

//Validation 검증가능한 인터페이스 생성
interface Validatable {...}

//위 인터페이스로 검증 함수 생성(검증함수는 검증 객체를 취해야 한다)
function validate(validatableInput: Validatable) {...}

//autobind decorator
function autobind(...){...}

//Component Base Class
//클래스의 속성, 메서드과 생성자함수의 코드들은 ProjectList class의 속성들을 그대로 써준다.
//제네릭 클래스로 만들면 상속받을 때 구현 타입을 정할 수 있다.
//속성의 타입이 한가지로 정해지는 게 아니므로 제네릭 타입 사용
//abstract : 추상키워드는 직접 인스턴스화가 이뤄지지 않는다는 것을 강조한다. 언제나 상속을 위해 쓰인다.
abstract class Component<T extends HTMLElement, U extends HTMLElement>{
    templateElement: HTMLTemplateElement;
    hostElement: T; //div요소일수도 아닐수도 있지만 html요소임은 확실하다.
    element: U; //form요소일수도 아닐수도 있지만 html요소임은 확실하다.

    //템플릿과 host의 id를 알아야 어떻게 선택하고 렌더링할지 알 수 있다.
    //newElement는 새롭게 렌더링된 요소에 id가 할당되도록 하는데, 선택적으로 만들기 위해 ?를 붙인다.
    constructor(templateId: string, hostElementId: string, insertAtStart: boolean, newElementId?: string){
        this.templateElement = document.getElementById(templateId)! as HTMLTemplateElement;
        this.hostElement = document.getElementById(hostElementId)! as T; //HTMLDivElement일 수도 있고 아닐수도 있다.

        const importedNode = document.importNode(this.templateElement.content, true);
    
        this.element = importedNode.firstElementChild as U; //HTMLElement일수도 있고 아닐수도 있다.
    
        //newElementId는 전달인자로 받아온 경우에만 element에 할당한다.
        if(newElementId) this.element.id = newElementId;

        this.attach(insertAtStart); //insertAtStart 매개변수는 true/false에따라 'afterbegin':'beforeend'이 결정된다.
    }
    private attach(insertAtBeginning: boolean){ 
        this.hostElement.insertAdjacentElement(insertAtBeginning ? 'afterbegin':'beforeend', this.element);
    }
    //실제 구현이 되지 않는 메서드
    //컴포넌트클래스에서 상속받는 모든 클래스에 이 두 메서드를 추가시킬 수 있다.
    abstract configure?(): void;
    abstract renderContent(): void;
}


//ProjectList Class : active/finished projectlist를 생성하는 클래스
class ProjectList extends Component<HTMLDivElement, HTMLElement>{ //이제  ProjectList class는 Component 클래스를 상속받을 수 있다. 
    //상속하는 클래스와 동일한 프로퍼티는 삭제
    //templateElement: HTMLTemplateElement;
    //hostElement: HTMLDivElement;
    //element: HTMLElement;
    assignedProjects: Project[]; 

    constructor(private type: 'active'|'finished'){
        super("project-list", "app", false, `${type}-projects`); //상속하는 클래스에서 생성자의 베이스 클래스를 불러온다.
        //전달인자는 id
        this.assignedProjects = []

        projectState.addListner((projects: Project[])=>{
            const relevantProjects = projects.filter(prj => {
                if(this.type === 'active') return prj.status === ProjectStatus.Active;
                else return prj.status === ProjectStatus.Finished; 
            });
            this.assignedProjects = relevantProjects; 
            this.renderProjects();
        });

        //상속하는 클래스와 중복된 메서드인 경우 삭제
        //this.attach();
        this.renderContent();
        this.configure();
    }
    //바로 위의 projectState.addListner가 설정된것을 가지고 configure에 접근해서 작업
    configure() {
        projectState.addListener((projects: Project[])=>{
        const relevantProjects = projects.filter(prj => {
         if(this.type === 'active') return prj.status === ProjectStatus.Active;
        else return prj.status === ProjectStatus.Finished; 
        });
        this.assignedProjects = relevantProjects; 
        this.renderProjects();
        });
    }


    //처음엔 private키워드를 사용하였는데, 비공개 추상 메소드는 지원이 안되므로 제외한다.
    renderContent(){
        const listId = `${this.type}-projects-list`;
        
        this.element.querySelector("ul")!.id = listId; 
        this.element.querySelector("h2")!.textContent = this.type.toUpperCase() + 'PROJECTS'; 
    }


    private renderProjects(){ 
    const listEl = document.getElementById(`${this.type}-projects-list`)! as HTMLUListElement;
    listEl.innerHTML = '';

    for(const prjItem of this.assignedProjects){
        const listItem = document.createElement('li');
        listItem.textContent = prjItem.title; 
        listEl.appendChild(listItem); 
    }
    }
}



//ProjectInput Class : form생성과 사용자입력값 수집을 담당
class ProjectInput extends Component<HTMLDivElement, HTMLFormElement>{
    //상속하는 클래스에도 있던 속성(proproperty) 삭제
    //templateElement: HTMLTemplateElement;
    //hostElement: HTMLDivElement;
    //element: HTMLFormElement;
    
    //타입스크립트는 이 속성들이 생성자 초기화에서 configure에 불러온다는 것을 확인하지 못한다.
    //해결 : 이 초기화를 생성자로 옮겨야 한다.
    titleInputElement: HTMLInputElement;
    descriptionInputElement: HTMLInputElement;
    peopleInputElement: HTMLInputElement;

    constructor(){
        super('project-input', 'app', true, 'user-input'); //생성 id를 전달인자로 모두 받는다.
        //전에 있던 id생성 코드는 모두 삭제
        this.titleInputElement = this.element.querySelector("#title") as HTMLInputElement;
        this.descriptionInputElement = this.element.querySelector("#description") as HTMLInputElement;
        this.peopleInputElement = this.element.querySelector("#people") as HTMLInputElement;

        this.configure();
        //this.attach(); //attach메서드는 상속하는 클래스가 해주는 메서드이므로 삭제
    }
    //submit이벤트를 생성하는 메서드
    //공개 메서드라 가장 앞에 둔다.
    configure(){
        this.element.addEventListener('submit', this.submitHandler);
    }

    //renderContent 메서드가 없어서 오류가 뜨므로 메서드 이름만 추가 -> 오류해결!
    renderContent(){}

    //입력값의 유효성 검사, 유효하면 입력값을 튜플형태로 반환
    private gatherUserInput(): [string, string, number] | void {
        const enteredTitle = this.titleInputElement.value;
        const enteredDescription = this.descriptionInputElement.value;
        const enteredPeople = this.peopleInputElement.value;
    
        const titleValidatable: Validatable = {
          value: enteredTitle,
          required: true
        };
        const descriptionValidatable: Validatable = {
          value: enteredDescription,
          required: true,
          minLength: 5
        };
        const peopleValidatable: Validatable = {
          value: +enteredPeople,
          required: true,
          min: 1,
          max: 5
        };
    
        if (
          !validate(titleValidatable) ||
          !validate(descriptionValidatable) ||
          !validate(peopleValidatable)
        ) {
          alert('Invalid input, please try again!');
          return;
        } else {
          return [enteredTitle, enteredDescription, +enteredPeople];
        }
      }

    //submit 클릭후 입력값이 삭제되는 메서드
    private clearInputs(){
        this.titleInputElement.value = '';
        this.descriptionInputElement.value = '';
        this.peopleInputElement.value = '';
    }

    //데코레이터
    @autobind 
    //submit버튼 클릭시 실행되는 메서드
    private submitHandler(event: Event){
        event.preventDefault();
        const userInput = this.gatherUserInput();

        if(Array.isArray(userInput)){
            const [title, desc, people] = userInput;
            console.log(title,desc, people);
            projectState.addProject(title, desc, people);
            this.clearInputs(); 
        }
    }
}

const PrjInput = new ProjectInput(); //프로젝트입력값 객체를 콘솔에 찍었다.
const activePrjList = new ProjectList('active'); //active project 객체
const finishedPrjList = new ProjectList('finished'); //finished project 객체

클래스로 프로젝트 항목 렌더링

프로젝트목록 항목을 클래스로 따로 만들어서 렌더링하는 단계
원래의 렌더링된 projectlist안에서 listitem을 만들어내는 것이 아니라
projectlistitem 클래스를 인스턴스화해서 그 projectlistitem class의 생성자에서 초기화하기
그리고 현재 title만 렌더링되는데, description과 people도 렌더링하고자 한다.

//index.html : html의 <li>태그 안에 <h2><h3><p>태그를 넣는다.
//이 태그들은 차례대로 title, people, description이 렌더링될 자리이다.
 <template id="single-project">
      <li>
        <h2></h2>
        <h3></h3>
        <p></p>
      </li>
    </template>
//enum은 두개의 식별자 옵션을 취할 때 사용
enum ProjectStatus {...}

//Project Class: 항상 동일한 구조를 갖는 프로젝트 객체 구축 방법을 취하는 클래스
class Project{...}

//새로운 커스텀 type listener 추가 : Listener은 listener함수의 배열
type Listener<T> = (items: T[]) => void; 

class State<T> {...}

//project state management class : 앱 상태를 관리하는 클래스, 전역상태관리 객체를 만든다.
class ProjectState extends State<Project>{...}
    
//projectState 새로운 객체 생성
const projectState = ProjectState.getInstance();

//Validation 검증가능한 인터페이스 생성
interface Validatable {...}
  
//위 인터페이스로 검증 함수 생성(검증함수는 검증 객체를 취해야 한다)
function validate(validatableInput: Validatable) {...}  

//autobind decorator
function autobind(...){...}

//Component Base Class(abstract) : 렌더링할 클래스의 상속을 위해 쓰인다.
abstract class Component<T extends HTMLElement, U extends HTMLElement> {...}

//ProjectItem Class : 단일 신규 프로젝트 항목을 렌더링하는 클래스(ul.lu로)
//이 클래스도 렌더링하기 위한 클래스이므로 상속받을 컴포넌트 클래스가 필요하다.
class ProjectItem extends Component<HTMLUListElement, HTMLLIElement>{
  private project: Project; //Project클래스를 기반으로 project를 저장한다.

  constructor(hostId: string, project: Project){
    super('single-project', hostId, false, project.id); //렌더링할 요소의 id
    //템플릿id, host id, 목록에서 요소가 맨앞과 맨뒤중 어디에 렌더링되어야 하는가 - false:맨뒤, 모든 project에는 id가 있다.
    this.project = project;

    this.configure();
    this.renderContent();
  }
  //상속받은 base class에서 필요한 메소드를 불러온다.
  configure(){}

  ////프로젝트 이름 뿐 아니라 설명, 사람수 모두 들어가야 한다.
  renderContent(){
    this.element.querySelector('h2').textContent = this.project.title;
    this.element.querySelector('h3').textContent = this.project.people.toString();
    this.element.querySelector('p').textContent = this.project.description;
    //이제 이 project를 projectlist로 가서 제대로 렌더링되게 만들어야 한다.
  }
}


//ProjectList Class : active/finished projectlist를 생성하는 클래스
class ProjectList extends Component<HTMLDivElement, HTMLElement>{ 
    assignedProjects: Project[]; //project배열 목록이 있는 곳

    constructor(private type: 'active'|'finished'){
        super("project-list", "app", false, `${type}-projects`); 
     
        this.assignedProjects = []

        projectState.addListner((projects: Project[])=>{
            const relevantProjects = projects.filter(prj => {
                if(this.type === 'active') return prj.status === ProjectStatus.Active;
                else return prj.status === ProjectStatus.Finished; 
            });
            this.assignedProjects = relevantProjects; 
            this.renderProjects();
        });
        this.renderContent();
        this.configure();
    }
    configure() {
        projectState.addListener((projects: Project[])=>{
        const relevantProjects = projects.filter(prj => {
         if(this.type === 'active') return prj.status === ProjectStatus.Active;
        else return prj.status === ProjectStatus.Finished; 
        });
        this.assignedProjects = relevantProjects; 
        this.renderProjects();
        });
    }

    renderContent(){
        const listId = `${this.type}-projects-list`;
        
        this.element.querySelector("ul")!.id = listId; 
        this.element.querySelector("h2")!.textContent = this.type.toUpperCase() + 'PROJECTS'; 
    }

    private renderProjects(){ 
    const listEl = document.getElementById(`${this.type}-projects-list`)! as HTMLUListElement;
    listEl.innerHTML = '';

    //listitem을 수동으로 만드는 대신 projectItem 클래스를 불러온다.
    /*for(const prjItem of this.assignedProjects){
        const listItem = document.createElement('li');
        listItem.textContent = prjItem.title; 
        listEl.appendChild(listItem); 
    }*/
    for(const prjItem of this.assignedProjects){
      new ProjectItem(this.element.querySelector('ul')!.id, prjItem); //ul의Id, projectItem이 전달인자로 들어간다.
    }
    }
}


//ProjectInput Class : form생성과 사용자입력값 수집을 담당
class ProjectInput extends Component<HTMLDivElement, HTMLFormElement>{
    titleInputElement: HTMLInputElement;
    descriptionInputElement: HTMLInputElement;
    peopleInputElement: HTMLInputElement;

    constructor(){
        super('project-input', 'app', true, 'user-input'); 
        //전에 있던 id생성 코드는 모두 삭제
      this.titleInputElement = this.element.querySelector("#title") as HTMLInputElement;
        this.descriptionInputElement = this.element.querySelector("#description") as HTMLInputElement;
        this.peopleInputElement = this.element.querySelector("#people") as HTMLInputElement;

        this.configure();
    }
    configure(){
        this.element.addEventListener('submit', this.submitHandler);
    }
    renderContent(){}

    private gatherUserInput(): [string, string, number] | void {...}

    //submit 클릭후 입력값이 삭제되는 메서드
    private clearInputs(){...}

    //데코레이터
    @autobind 
    //submit버튼 클릭시 실행되는 메서드
    private submitHandler(event: Event){...}
}

const PrjInput = new ProjectInput(); //프로젝트입력값 객체를 콘솔에 찍었다.
const activePrjList = new ProjectList('active'); //active project 객체
const finishedPrjList = new ProjectList('finished'); //finished project 객체

게터 사용하기

//ProjectItem Class : 단일 신규 프로젝트 항목을 렌더링하는 클래스(ul,li로)
class ProjectItem extends Component<HTMLUListElement, HTMLLIElement>{
  private project: Project; 

  //people을 렌더링할 때 추가로 옆에 'Persons assigned'를 붙이고 싶은데, 1명일 경우에은 person으로 변경해야한다.
  //이때 게터가 도움이 된다.
  get persons(){
    if(this.project.people===1) return '1 person';
    else return `${this.project.people} persons`;
  }

  constructor(hostId: string, project: Project){
    super('single-project', hostId, false, project.id); 
    this.project = project;

    this.configure();
    this.renderContent();
  }
  configure(){}

  renderContent(){
    this.element.querySelector('h2').textContent = this.project.title;
    this.element.querySelector('h3').textContent = this.persons + 'assigned'; //원래 : this.project.people.toString() + 'assigned';
    this.element.querySelector('p').textContent = this.project.description;
  }
}

드래그앤드롭 구현을 위한 인터페이스 활용하기

각 프로젝트 목록 박스에 있는 항목을 드래그앤드롭으로 옮기는 기능을 추가하는 단계
보이는 UI부부분만 업데이트하는 것이 아닌,
프로젝트 목록을 관리하는 프로젝트상태관리 클래스에서 보이지 않는 부분까지 조정해야 한다.
모든 프로젝트에는 상태 프로퍼티가 있으므로 그것을 업데이트 해주어야 한다.

//Drag & Drop Interfaces
//일부 객체의 구조를 단순히 정의하기 위함이 아닌,
//어떤 클래스들이 이 클래스들로 하여금 특정 메소드를 실행하도록 하는 일종의 계약
//인터페이스는 2개, 드래깅가능한 인터페이스 / 드래그타겟이되는 인터페이스
//ProjectItem를 드래깅가능하게 만든다.
//HTML파일에도 요소가 draggable이 된다고 저장하면(draggable="true"),
//그 요소는 이제 드래그가 가능하다.
interface Draggable {
  //드래그앤드롭을 실행할 때, 드래그 대상은 이벤트리스너를 필요로 한다.
  dragStartHandler(event: DragEvent): void;
  dragEndHandler(event: DragEvent): void;
}
//ProjectList는 드래그된 ProjectItem을 둘 타겟이된다.
interface DragTarget{
  //드래그가 유효한 타겟임을 알려준다.
  dragOverHandler(event: DragEvent): void;
  //실제 일어나는 드롭에 대응한다.
  dropHandler(event: DragEvent): void;
  //박스위에 뭔가 드래그할 때 배경색 바꾸기 등 비주얼 피드백을 줄 수 있다.
  dragLeaveHandler(event: DragEvent): void;
}

...

//ProjectItem Class : 단일 신규 프로젝트 항목을 렌더링하는 클래스(ul.li로)
//implements Draggable를 사용하면 드래깅가능한 인터페이스를 실행하게 된다.
//에러가 나는데, 새로운 메서드를 추가하게 한다.
class ProjectItem extends Component<HTMLUListElement, HTMLLIElement>
  implements Draggable{
  private project: Project; 

  get persons(){
    if(this.project.people===1) return '1 person';
    else return `${this.project.people} persons`;
  }

  constructor(hostId: string, project: Project){
    super('single-project', hostId, false, project.id); 
    this.project = project;

    this.configure();
    this.renderContent();
  }
  @autobind //dragStartHandler가 클래스를 가르키게 하고 싶으므로 bind 데코레이터 사용
  dragStartHandler(event: DragEvent){
    console.log(event);
  }
  dragEndHandler(_: DragEvent){ //매개변수를 비워두어 ts에 사용하지 않고 있음을 알려준다.
    console.log('DragEnd')
  }    

  configure(){
    this.element.addEventListener('dragstart', this.dragStartHandler);
    this.element.addEventListener('dragend', this.dragEndHandler);
    //dragstart,dragend m: 디폴트 브라우저 dom 이벤트

  }

  renderContent(){
    this.element.querySelector('h2').textContent = this.project.title;
    this.element.querySelector('h3').textContent = this.persons + 'assigned'; 
    this.element.querySelector('p').textContent = this.project.description;
  }
}
//index.html 파일에서 수정
<template id="single-project">
      <li draggable="true">
        <h2></h2>
        <h3></h3>
        <p></p>
      </li>
    </template>

드래그 이벤트 및 UI현재 상태 반영하기

드롭타겟을 작업하여 프로젝트를 드래깅 가능한 박스로 추가하게 만드는 단계
드래그하면 프로젝트목록의 배경색이 바뀌어 드래깅가능한 박스로 인식하게 만든다.

//Drag & Drop Interfaces
interface Draggable {
  dragStartHandler(event: DragEvent): void;
  dragEndHandler(event: DragEvent): void;
}

interface DragTarget{
  //드래그가 유효한 타겟임을 알려준다.
  dragOverHandler(event: DragEvent): void;
  //실제 일어나는 드롭에 대응한다.
  dropHandler(event: DragEvent): void;
  //박스위에 뭔가 드래그할 때 배경색 바꾸기 등 비주얼 피드백을 줄 수 있다.
  dragLeaveHandler(event: DragEvent): void;
}

//ProjectList Class : active/finished projectlist를 생성하는 클래스
//implements DragTarget 인터페이스 사용, DragTarget의 메소드를 써야 한다.class ProjectList extends Component<HTMLDivElement, HTMLElement> implements DragTarget{ 
    assignedProjects: Project[]; 

    constructor(private type: 'active'|'finished'){
        super("project-list", "app", false, `${type}-projects`); 
        this.assignedProjects = []
        
        this.renderContent();
        this.configure();
    }
    @autobind
    dragOverHandler(_: DragEvent){
      const listEl = this.element.querySelector('ul')!;//처음에는 ul이 없을수도 있으므로
      listEl.classList.add('droppable'); //css에 이미 droppable 스타일링이 있다.
      //드래그 한다음 드롭하기 위한 장소가 어딘지 모르므로, 드롭가능한 장소를 css를 통해 알려준다.
    }

    dropHandler(_: DragEvent){}

    @autobind
    dragLeaveHandler(_: DragEvent){
      const listEl = this.element.querySelector('ul')!;
      listEl.classList.remove('droppable'); //드래그를 멈추고 요소를 떠날 때 스타일링 업데이트 
    }


    ⭐configure() {
        this.element.addEventListener('dragover', this.dragOverHandler);
        this.element.addEventListener('dragleave', this.dragLeaveHandler);
        this.element.addEventListener('drop', this.dropHandler);

        projectState.addListener((projects: Project[])=>{
        const relevantProjects = projects.filter(prj => {
         if(this.type === 'active') {
          return prj.status === ProjectStatus.Active;
        } else{
          return prj.status === ProjectStatus.Finished;
        }
        });
        this.assignedProjects = relevantProjects; 
        this.renderProjects();
        });
    }

    renderContent(){
        const listId = `${this.type}-projects-list`;
        
        this.element.querySelector("ul")!.id = listId; 
        this.element.querySelector("h2")!.textContent = this.type.toUpperCase() + 'PROJECTS'; 
    }


    private renderProjects() {
      const listEl = document.getElementById(
        `${this.type}-projects-list`
      )! as HTMLUListElement;
      listEl.innerHTML = '';
      for (const prjItem of this.assignedProjects) {
        new ProjectItem(this.element.querySelector('ul')!.id, prjItem);
      }
    }
  }

드롭할 수 있는 영역 추가하기

드래그 하고 드롭 할 때 어느 프로젝트 목록에 드롭한지 알 수 있게 하는 단계

//Drag & Drop Interfaces
interface Draggable {
  dragStartHandler(event: DragEvent): void;
  dragEndHandler(event: DragEvent): void;
}

interface DragTarget{
  dragOverHandler(event: DragEvent): void;
  dropHandler(event: DragEvent): void;
  dragLeaveHandler(event: DragEvent): void;
}

//ProjectItem Class : 단일 신규 프로젝트 항목을 렌더링하는 클래스(ul.li로)
class ProjectItem extends Component<HTMLUListElement, HTMLLIElement>
  implements Draggable{
  private project: Project; 

  get persons(){
    if(this.project.people===1) return '1 person';
    else return `${this.project.people} persons`;
  }

  constructor(hostId: string, project: Project){
    super('single-project', hostId, false, project.id); 
    this.project = project;

    this.configure();
    this.renderContent();
  }
  @autobind 
  dragStartHandler(event: DragEvent){
    event.dataTransfer!.setData('text/plain', this.project.id); //데이터를 드래그이벤트에 붙일 수 있다. 드롭 후에 데이터를 추출할 수 있다.
    //setData의 첫번째 인자는 데이터포맷 식별자
    event.dataTransfer!.effectAllowed = 'move'; //커서의 모양 조절, 브라우저에게 의도를 알려줄 수 있다.
  }
  dragEndHandler(_: DragEvent){ 
    console.log('DragEnd')
  }    

  configure(){
    this.element.addEventListener('dragstart', this.dragStartHandler);
    this.element.addEventListener('dragend', this.dragEndHandler);
  }

  renderContent(){
    this.element.querySelector('h2').textContent = this.project.title;
    this.element.querySelector('h3').textContent = this.persons + 'assigned'; 
    this.element.querySelector('p').textContent = this.project.description;
  }
}


//ProjectList Class : active/finished projectlist를 생성하는 클래스
class ProjectList extends Component<HTMLDivElement, HTMLElement> implements DragTarget{ 
    assignedProjects: Project[]; 

    constructor(private type: 'active'|'finished'){
        super("project-list", "app", false, `${type}-projects`); 
        this.assignedProjects = []
        
        this.renderContent();
        this.configure();
    }
    @autobind
    dragOverHandler(event: DragEvent){
      //데이터포맷이 다른 드로핑은 허용하지 않고, 플레인텍스트에서만 드로핑하게 허용
      if(event.dataTransfer && event.dataTransfer.types[0]==='text/plain'){
        event.preventDefault(); //js의 드래그앤드롭이벤트의 디폴트는 드로핑을 허용하지 않는다. 그 반대로 해야하므로 기본동작을 막아준다.
        const listEl = this.element.querySelector('ul')!;
        listEl.classList.add('droppable'); 
      }
    }

    dropHandler(event: DragEvent){
      console.log(event.dataTransfer!.getData('text/plain'));
    }

    @autobind
    dragLeaveHandler(_: DragEvent){
      const listEl = this.element.querySelector('ul')!;
      listEl.classList.remove('droppable');
    }


    configure() {
        this.element.addEventListener('dragover', this.dragOverHandler);
        this.element.addEventListener('dragleave', this.dragLeaveHandler);
        this.element.addEventListener('drop', this.dropHandler);

        projectState.addListener((projects: Project[])=>{
        const relevantProjects = projects.filter(prj => {
         if(this.type === 'active') {
          return prj.status === ProjectStatus.Active;
        } else{
          return prj.status === ProjectStatus.Finished;
        }
        });
        this.assignedProjects = relevantProjects; 
        this.renderProjects();
        });
    }

    renderContent(){
        const listId = `${this.type}-projects-list`;
        
        this.element.querySelector("ul")!.id = listId; 
        this.element.querySelector("h2")!.textContent = this.type.toUpperCase() + 'PROJECTS'; 
    }


    private renderProjects() {
      const listEl = document.getElementById(
        `${this.type}-projects-list`
      )! as HTMLUListElement;
      listEl.innerHTML = '';
      for (const prjItem of this.assignedProjects) {
        new ProjectItem(this.element.querySelector('ul')!.id, prjItem);
      }
    }
  }

드래그 앤 드롭 마무리하기

뒤에서 상태를 업데이트하고 UI를 재렌더링하는 단계

//Drag & Drop Interfaces
interface Draggable {
  dragStartHandler(event: DragEvent): void;
  dragEndHandler(event: DragEvent): void;
}

interface DragTarget{
  dragOverHandler(event: DragEvent): void;
  dropHandler(event: DragEvent): void;
  dragLeaveHandler(event: DragEvent): void;
}

//project state management class : 앱 상태를 관리하는 클래스, 전역상태관리 객체를 만든다.
class ProjectState extends State<Project>{
    private projects: Project[] = []; 
    private static instance: ProjectState;

    private constructor(){
        super();
    }

    static getInstance(){
        if(this.instance) return this.instance;
        this.instance = new ProjectState();
        return this.instance;
    }

    addProject(title: string, description: string, numOfPeople: number) {
    const newProject = new Project(
      Math.random().toString(),
      title,
      description,
      numOfPeople,
      ProjectStatus.Active
    );
    this.projects.push(newProject);
    this.updateListener();
  }
  //project 상태 변경 메서드 : project를 현재 있는 목록에서 새로운 목록으로 옮기는 것
  //언제나 진행중에서 완료로 변경하는 것은 아니다. 동일한 박스에서 드래그앤드롭을 실행할수도 있다.
  moveProject(projectId: string, newStatus: ProjectStatus){
    const project = this.projects.find(prj => prj.id === projectId); //배열 중 조건에 맞는 요소를 찾는 배열메서드
    if(project && project.status!==newStatus) project.status = newStatus;
    //프로젝트가 있다면 프로젝트의 상태를 새로운 상태로 변경
    //드래그앤드롭 할때마다 업데이트해서 렌더링을 하는데,
    //사실 실제로 변화한게 없다면 업데이트하지 않아도 된다.
    //어떤 상태가 실제로 변화했는지 확인하고, 변회하지 않았다면 업데이트를 하지 않아야 한다.
    this.updateListener();
  }

  //모든 리스너들을 불러오는 메서드, 리스너는 항목을 재렌더링할 목록으로 이어진다.
  private updateListener(){
    for (const listenerFn of this.listeners) {
      listenerFn(this.projects.slice());
    }
  }
}
//projectState 새로운 객체 생성
const projectState = ProjectState.getInstance();

//ProjectItem Class : 단일 신규 프로젝트 항목을 렌더링하는 클래스(ul.li로)
class ProjectItem extends Component<HTMLUListElement, HTMLLIElement>
  implements Draggable{
  private project: Project; 

  get persons(){
    if(this.project.people===1) return '1 person';
    else return `${this.project.people} persons`;
  }

  constructor(hostId: string, project: Project){
    super('single-project', hostId, false, project.id); 
    this.project = project;

    this.configure();
    this.renderContent();
  }
  @autobind 
  dragStartHandler(event: DragEvent){
    event.dataTransfer!.setData('text/plain', this.project.id); 
    event.dataTransfer!.effectAllowed = 'move'; 
  }
  dragEndHandler(_: DragEvent){ 
    console.log('DragEnd')
  }    

  configure(){
    this.element.addEventListener('dragstart', this.dragStartHandler);
    this.element.addEventListener('dragend', this.dragEndHandler);
  }

  renderContent(){
    this.element.querySelector('h2').textContent = this.project.title;
    this.element.querySelector('h3').textContent = this.persons + 'assigned'; 
    this.element.querySelector('p').textContent = this.project.description;
  }
}


//ProjectList Class : active/finished projectlist를 생성하는 클래스
class ProjectList extends Component<HTMLDivElement, HTMLElement> implements DragTarget{ 
    assignedProjects: Project[]; 

    constructor(private type: 'active'|'finished'){
        super("project-list", "app", false, `${type}-projects`); 
        this.assignedProjects = []
        
        this.renderContent();
        this.configure();
    }
    @autobind
    dragOverHandler(event: DragEvent){
      if(event.dataTransfer && event.dataTransfer.types[0]==='text/plain'){
        event.preventDefault(); 
        const listEl = this.element.querySelector('ul')!;
        listEl.classList.add('droppable'); 
      }
    }

    @autobind
    dropHandler(event: DragEvent){
      //console.log(event.dataTransfer!.getData('text/plain'));
      const prjId = event.dataTransfer!.getData('text/plain'); //project id를 추출
      projectState.moveProject(prjId, this.type==='active' ? ProjectStatus.Active:ProjectStatus.Finished);
    }

    @autobind
    dragLeaveHandler(_: DragEvent){
      const listEl = this.element.querySelector('ul')!;
      listEl.classList.remove('droppable');
    }


    configure() {
        this.element.addEventListener('dragover', this.dragOverHandler);
        this.element.addEventListener('dragleave', this.dragLeaveHandler);
        this.element.addEventListener('drop', this.dropHandler);

        projectState.addListener((projects: Project[])=>{
        const relevantProjects = projects.filter(prj => {
         if(this.type === 'active') {
          return prj.status === ProjectStatus.Active;
        } else{
          return prj.status === ProjectStatus.Finished;
        }
        });
        this.assignedProjects = relevantProjects; 
        this.renderProjects();
        });
    }

    renderContent(){
        const listId = `${this.type}-projects-list`;
        
        this.element.querySelector("ul")!.id = listId; 
        this.element.querySelector("h2")!.textContent = this.type.toUpperCase() + 'PROJECTS'; 
    }


    private renderProjects() {
      const listEl = document.getElementById(
        `${this.type}-projects-list`
      )! as HTMLUListElement;
      listEl.innerHTML = '';
      for (const prjItem of this.assignedProjects) {
        new ProjectItem(this.element.querySelector('ul')!.id, prjItem);
      }
    }
  }

전체 코드

//Drag & Drop Interfaces
interface Draggable {
  dragStartHandler(event: DragEvent): void;
  dragEndHandler(event: DragEvent): void;
}

interface DragTarget{
  dragOverHandler(event: DragEvent): void;
  dropHandler(event: DragEvent): void;
  dragLeaveHandler(event: DragEvent): void;
}


//enum은 두개의 식별자 옵션을 취할 때 사용
enum ProjectStatus {
    Active,
    Finished
}

//Project Class: 항상 동일한 구조를 갖는 프로젝트 객체 구축 방법을 취하는 클래스
class Project{
    constructor(
        public id: string, 
        public title: string, 
        public description: string, 
        public people: number, 
        public status: ProjectStatus){
    }
}

//새로운 커스텀 type listener 추가 : Listener은 listener함수의 배열
type Listener<T> = (items: T[]) => void; 

class State<T> {
  protected listeners: Listener<T>[] = [];

  addListener(listenerFn: Listener<T>) {
    this.listeners.push(listenerFn);
  }
} 

//project state management class : 앱 상태를 관리하는 클래스, 전역상태관리 객체를 만든다.
class ProjectState extends State<Project>{
    private projects: Project[] = []; 
    private static instance: ProjectState;

    private constructor(){
        super();
    }

    static getInstance(){
        if(this.instance) return this.instance;
        this.instance = new ProjectState();
        return this.instance;
    }

    addProject(title: string, description: string, numOfPeople: number) {
    const newProject = new Project(
      Math.random().toString(),
      title,
      description,
      numOfPeople,
      ProjectStatus.Active
    );
    this.projects.push(newProject);
    this.updateListener();
  }
  //project 상태 변경 메서드 : project를 현재 있는 목록에서 새로운 목록으로 옮기는 것
  moveProject(projectId: string, newStatus: ProjectStatus){
    const project = this.projects.find(prj => prj.id === projectId);
    if(project && project.status!==newStatus) project.status = newStatus;
    this.updateListener();
  }

  private updateListener(){
    for (const listenerFn of this.listeners) {
      listenerFn(this.projects.slice());
    }
  }
}
//projectState 새로운 객체 생성
const projectState = ProjectState.getInstance();

//Validation 검증가능한 인터페이스 생성
interface Validatable {
    value: string | number;
    required?: boolean;
    minLength?: number;
    maxLength?: number;
    min?: number;
    max?: number;
  }

//위 인터페이스로 검증 함수 생성(검증함수는 검증 객체를 취해야 한다)
function validate(validatableInput: Validatable) {
    let isValid = true;
    if (validatableInput.required) {
      isValid = isValid && validatableInput.value.toString().trim().length !== 0;
    }
    if (
      validatableInput.minLength != null &&
      typeof validatableInput.value === 'string'
    ) {
      isValid =
        isValid && validatableInput.value.length >= validatableInput.minLength;
    }
    if (
      validatableInput.maxLength != null &&
      typeof validatableInput.value === 'string'
    ) {
      isValid =
        isValid && validatableInput.value.length <= validatableInput.maxLength;
    }
    if (
      validatableInput.min != null &&
      typeof validatableInput.value === 'number'
    ) {
      isValid = isValid && validatableInput.value >= validatableInput.min;
    }
    if (
      validatableInput.max != null &&
      typeof validatableInput.value === 'number'
    ) {
      isValid = isValid && validatableInput.value <= validatableInput.max;
    }
    return isValid;
}


//autobind decorator
function autobind(_: any, _2: string, descriptor: PropertyDescriptor){
    const originalMethod = descriptor.value;

    const adjDescriptor: PropertyDescriptor = {
        configurable: true, 
        get(){
            const boundFn = originalMethod.bind(this);
            return boundFn;
        }
    };
    return adjDescriptor;
}


//Component Base Class(abstract) : 렌더링할 클래스의 상속을 위해 쓰인다.
abstract class Component<T extends HTMLElement, U extends HTMLElement> {
    templateElement: HTMLTemplateElement;
    hostElement: T; 
    element: U; 

    constructor(templateId: string, hostElementId: string, insertAtStart: boolean, newElementId?: string){
        this.templateElement = document.getElementById(templateId)! as HTMLTemplateElement;
        this.hostElement = document.getElementById(hostElementId)! as T; 

        const importedNode = document.importNode(
          this.templateElement.content,
          true
        );
    
        this.element = importedNode.firstElementChild as U; 
    
        if(newElementId) this.element.id = newElementId;

        this.attach(insertAtStart); 
    }
    private attach(insertAtBeginning: boolean){ 
        this.hostElement.insertAdjacentElement(insertAtBeginning ? 'afterbegin':'beforeend', this.element);
    }

    abstract configure?(): void;
    abstract renderContent(): void;
}


//ProjectItem Class : 단일 신규 프로젝트 항목을 렌더링하는 클래스(ul.li로)
class ProjectItem extends Component<HTMLUListElement, HTMLLIElement>
  implements Draggable{
  private project: Project; 

  get persons(){
    if(this.project.people===1) return '1 person';
    else return `${this.project.people} persons`;
  }

  constructor(hostId: string, project: Project){
    super('single-project', hostId, false, project.id); 
    this.project = project;

    this.configure();
    this.renderContent();
  }
  @autobind 
  dragStartHandler(event: DragEvent){
    event.dataTransfer!.setData('text/plain', this.project.id); 
    event.dataTransfer!.effectAllowed = 'move'; 
  }
  dragEndHandler(_: DragEvent){ 
    console.log('DragEnd')
  }    

  configure(){
    this.element.addEventListener('dragstart', this.dragStartHandler);
    this.element.addEventListener('dragend', this.dragEndHandler);
  }

  renderContent(){
    this.element.querySelector('h2').textContent = this.project.title;
    this.element.querySelector('h3').textContent = this.persons + 'assigned'; 
    this.element.querySelector('p').textContent = this.project.description;
  }
}


//ProjectList Class : active/finished projectlist를 생성하는 클래스
class ProjectList extends Component<HTMLDivElement, HTMLElement> implements DragTarget{ 
    assignedProjects: Project[]; 

    constructor(private type: 'active'|'finished'){
        super("project-list", "app", false, `${type}-projects`); 
        this.assignedProjects = []
        
        this.renderContent();
        this.configure();
    }
    @autobind
    dragOverHandler(event: DragEvent){
      if(event.dataTransfer && event.dataTransfer.types[0]==='text/plain'){
        event.preventDefault(); 
        const listEl = this.element.querySelector('ul')!;
        listEl.classList.add('droppable'); 
      }
    }

    @autobind
    dropHandler(event: DragEvent){
      const prjId = event.dataTransfer!.getData('text/plain');
      projectState.moveProject(prjId, this.type==='active' ? ProjectStatus.Active:ProjectStatus.Finished);
    }

    @autobind
    dragLeaveHandler(_: DragEvent){
      const listEl = this.element.querySelector('ul')!;
      listEl.classList.remove('droppable');
    }


    configure() {
        this.element.addEventListener('dragover', this.dragOverHandler);
        this.element.addEventListener('dragleave', this.dragLeaveHandler);
        this.element.addEventListener('drop', this.dropHandler);

        projectState.addListener((projects: Project[])=>{
        const relevantProjects = projects.filter(prj => {
         if(this.type === 'active') {
          return prj.status === ProjectStatus.Active;
        } else{
          return prj.status === ProjectStatus.Finished;
        }
        });
        this.assignedProjects = relevantProjects; 
        this.renderProjects();
        });
    }

    renderContent(){
        const listId = `${this.type}-projects-list`;
        
        this.element.querySelector("ul")!.id = listId; 
        this.element.querySelector("h2")!.textContent = this.type.toUpperCase() + 'PROJECTS'; 
    }


    private renderProjects() {
      const listEl = document.getElementById(
        `${this.type}-projects-list`
      )! as HTMLUListElement;
      listEl.innerHTML = '';
      for (const prjItem of this.assignedProjects) {
        new ProjectItem(this.element.querySelector('ul')!.id, prjItem);
      }
    }
  }


//ProjectInput Class : form생성과 사용자입력값 수집을 담당
class ProjectInput extends Component<HTMLDivElement, HTMLFormElement>{
    titleInputElement: HTMLInputElement;
    descriptionInputElement: HTMLInputElement;
    peopleInputElement: HTMLInputElement;

    constructor(){
        super('project-input', 'app', true, 'user-input'); 

        this.titleInputElement = this.element.querySelector("#title") as HTMLInputElement;
        this.descriptionInputElement = this.element.querySelector("#description") as HTMLInputElement;
        this.peopleInputElement = this.element.querySelector("#people") as HTMLInputElement;

        this.configure();
    }
    
    configure(){
        this.element.addEventListener('submit', this.submitHandler);
    }
    renderContent(){}

    private gatherUserInput(): [string, string, number] | void {
        const enteredTitle = this.titleInputElement.value;
        const enteredDescription = this.descriptionInputElement.value;
        const enteredPeople = this.peopleInputElement.value;
    
        const titleValidatable: Validatable = {
          value: enteredTitle,
          required: true
        };
        const descriptionValidatable: Validatable = {
          value: enteredDescription,
          required: true,
          minLength: 5
        };
        const peopleValidatable: Validatable = {
          value: +enteredPeople,
          required: true,
          min: 1,
          max: 5
        };
    
        if (
          !validate(titleValidatable) ||
          !validate(descriptionValidatable) ||
          !validate(peopleValidatable)
        ) {
          alert('Invalid input, please try again!');
          return;
        } else {
          return [enteredTitle, enteredDescription, +enteredPeople];
        }
      }

    //submit 클릭후 입력값이 삭제되는 메서드
    private clearInputs(){
        this.titleInputElement.value = '';
        this.descriptionInputElement.value = '';
        this.peopleInputElement.value = '';
    }

    //데코레이터
    @autobind 
    //submit버튼 클릭시 실행되는 메서드
    private submitHandler(event: Event){
        event.preventDefault();
        const userInput = this.gatherUserInput();

        if(Array.isArray(userInput)){
            const [title, desc, people] = userInput;
            console.log(title,desc, people);
            projectState.addProject(title, desc, people);
            this.clearInputs(); 
        }
    }
}

const PrjInput = new ProjectInput(); //프로젝트입력값 객체를 콘솔에 찍었다.
const activePrjList = new ProjectList('active'); //active project 객체
const finishedPrjList = new ProjectList('finished'); //finished project 객체
profile
신입 프론트엔드 웹 개발자입니다.

0개의 댓글

관련 채용 정보