Asp.net webapi와 Angular, Sqlite를 이용한 Jwt Login, Todo 만들기. #2

유튜브 : https://youtu.be/18YdKq7JvMs

Github : https://github.com/tigersarang/AspnetAngJwtSqliteTodo

참고 사이트 : https://www.udemy.com/share/101WMM3@BinRVzIc_oHnJ4yrGuBmUbzQ_OVNQifnzbfKlpDAVFGCTCIuCBysacegf38ijijm5g==/


angular 설치

ng new todo-app


1. 패키지 설치

npm install -force ngx-bootstrap bootstrap ngx-toastr @angular/animations @popperjs/core

2. angular.json에 css, js 추가하기

            "styles": [
              "node_modules/ngx-toastr/toastr.css",
                "node_modules/bootstrap/dist/css/bootstrap.min.css",
              "src/styles.css"
            ],
            "scripts": [
              "node_modules/@popperjs/core/dist/umd/popper.min.js",
              "node_modules/bootstrap/dist/js/bootstrap.min.js"
            ]
3. app.config.ts에 provider 추가

  import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
  import { provideRouter } from '@angular/router';
  import { routes } from './app.routes';
  import { provideHttpClient, withInterceptors } from '@angular/common/http';
  import {provideAnimations} from '@angular/platform-browser/animations';
  import { provideToastr } from 'ngx-toastr';
  import { authInterceptor } from './_interceptor/auth.interceptor';

    export const appConfig: ApplicationConfig = {
      providers: [
        provideZoneChangeDetection({ eventCoalescing: true }),
        provideRouter(routes),
        provideHttpClient(withInterceptors([authInterceptor])),
        provideAnimations(),
        provideToastr(
          {
            positionClass: 'toast-bottom-right',
            timeOut: 5000,
            closeButton: true
          }
        )
      ]
    };



4. app.component.ts 수정

import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
@Component({
  selector: 'app-root',
  imports: [RouterOutlet, BsDropdownModule, NavComponent],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent implements OnInit {
  title = 'todo-app';
  private customerService = inject(CustomerService);
  private autoLogoutService = inject(AutoLogoutService);

  setCurrentUser() {
    const userString = localStorage.getItem('user');

    if (userString) {
      const user = JSON.parse(userString);
      this.customerService.currentUser.set(user);
    }

    this.autoLogoutService.init();
  }

  ngOnInit(): void {
    this.setCurrentUser();
  }
}

5. app.component.html 수정

  <app-nav></app-nav>

  <router-outlet></router-outlet>

6. environment 파일 생성

ng g environments

environment.development.ts

    export const environment = {
      apiUrl: 'http://localhost:5213/api/'
    };

7. app.routes.ts 수정

  export const routes: Routes = [
    {path:'home', component: HomeComponent},
    {path:'register', component: RegisterComponent},
    {path:'login', component: LoginComponent},
    {path:'todo', component: TodoListComponent},
    {path:'todoAdd', component: TodoRegisterComponent},
    {path:'todoEdit', component: TodoEditComponent},
    {path:'todoDetail/:id', component: TodoDetailComponent},
    {path:'**', redirectTo: 'home', pathMatch:'full'}
  ];





8. model 생성

ng g i _models/todo
ng g i _models/customer


  export interface Customer {
    id: number;
    userName: string;
    email: string;
    password: string;
    token: string;
  }


  export interface Todo {
    id: number;
    title: string;
    description: string;
    isCompleted: boolean;
  }




9. service 생성

ng g s _services/todo
ng g s _services/customer
ng g s _services/auto-logout

    export class TodoService {
      private http = inject(HttpClient);
      apiUrl = environment.apiUrl;
      private todoToEditSource = new BehaviorSubject<Todo | null>(null);
      currentTodoToEdit = this.todoToEditSource.asObservable();

      add(todoItem : Todo) {
        return this.http.post<Todo>(this.apiUrl + 'todo/add', todoItem);
      }

      list() {
        return this.http.get(this.apiUrl + 'todo/all');
      }

      get(id: number) {
        return this.http.get(this.apiUrl + 'todo/' + id);
      }

      delete(id: number) {
        return this.http.delete(this.apiUrl + 'todo/'+ id);
      }

      update(todoItem: Todo) {
        return this.http.put<Todo>(this.apiUrl + 'todo/update', todoItem);
      }

      sendTodo(todo: Todo) {
        this.todoToEditSource.next(todo);
      }
    }

----------------------------------------------------------------------

  export class CustomerService {
    private http = inject(HttpClient);
    apiUrl = environment.apiUrl;
    currentUser = signal<Customer | null>(null);

    register(customer: Customer) {
      return this.http.post<Customer>(this.apiUrl + 'Customer/register', customer).pipe(
        map(user => {
          localStorage.setItem('user', JSON.stringify(user));
          this.currentUser.set(user);
        })
      )
    }

    login(customer: Customer) {
      return this.http.post<Customer>(this.apiUrl + 'Customer/login', customer).pipe(
        map(user => {
          localStorage.setItem('user', JSON.stringify(user));
          this.currentUser.set(user);
        })
      )
    }

    logout() {
      localStorage.removeItem('user');
      this.currentUser.set(null);
    }
  } 

----------------------------------------------------------------------


  const LOGOUT_MINUTE = 1;
  const CHECK_INTERVAL = 3000;
  const STORE_KEY = 'lastAction';



  @Injectable({
    providedIn: 'root'
  })
  export class AutoLogoutService  {

  private customerService = inject(CustomerService);
    private router = inject(Router);
    private ngZone = inject(NgZone);

    private intervaliId: any;

    constructor() {
      this.init();
    }

    init() {
      if (this.customerService.currentUser()) {
        this.reset();
        this.initListener();
        this.initInterval();
    }
    }

    public getLastAction() : number {
      const lastAction = localStorage.getItem(STORE_KEY);
      return lastAction ? parseInt(lastAction, 10) : Date.now();
    }

    public setLastAction(lastAction: number) : void {
      localStorage.setItem(STORE_KEY, lastAction.toString());
    }

    reset() {
      this.setLastAction(Date.now());
    }

    initListener() {
      this.ngZone.runOutsideAngular(() => {
        document.body.addEventListener('click', () => this.reset());
        document.body.addEventListener('mouseover', () => this.reset());
        document.body.addEventListener('mouseout', () => this.reset());
        document.body.addEventListener('keydown', () => this.reset());
        document.body.addEventListener('keyup', () => this.reset());
        document.body.addEventListener('keypress', () => this.reset());
      });
    }

    initInterval() {
      console.log("initInterval : ");
      this.ngZone.runOutsideAngular(() => {
        this.intervaliId = setInterval(() => {
          this.check();
        }, CHECK_INTERVAL);
      });
    }

    check() {
      const now = Date.now();
      const timeleft = this.getLastAction() + LOGOUT_MINUTE * 60 * 1000;
      const diff = timeleft - now;
      const isTimeout = diff < 0;

      this.ngZone.run(() => {
        console.log(`마지막 활동 후 ${LOGOUT_MINUTE}분이 지남`);

        if (isTimeout && this.customerService.currentUser()) {
          console.log(`마지막 활동 후 ${LOGOUT_MINUTE}분이 지남 logout 시작`);
          this.customerService.logout();

          if (this.intervaliId) {
            clearInterval(this.intervaliId);
          }
        }
      });
    }
  }



10. 컴포넌트 생성

참고 : FormsModule을 사용하는 곳에서는 다음 import를 추가해야 됩니다.

import { FormsModule } from '@angular/forms';

ng g c _components/customer/register
ng g c _components/customer/login
ng g c _components/home
ng g c _components/nav
ng g c _components/todo/todo_list
ng g c _components/todo/todo_register
ng g c _components/todo/todo_detail
ng g c _components/todo/todo_edit

  @Component({
    selector: 'app-register',
    imports: [FormsModule],
    templateUrl: './register.component.html',
    styleUrl: './register.component.css'
  })
  export class RegisterComponent {
    private customerService = inject(CustomerService);
    model: any = {}
    private toastrService = inject(ToastrService);

    register() {
      this.customerService.register(this.model).subscribe({
        next: (user) => {
          console.log(user);
          this.toastrService.success('Success');
        },
        error: (error) => {
          console.log(error);
          this.toastrService.success('Error');
        }
      })
    }
  }



  @Component({
    selector: 'app-login',
    imports: [FormsModule],
    templateUrl: './login.component.html',
    styleUrl: './login.component.css'
  })
  export class LoginComponent {
    private customerService = inject(CustomerService);
    model: any = {}
    private toastrService = inject(ToastrService);
    private router = inject(Router);

    login() {
      this.customerService.login(this.model).subscribe({
        next: (user) => {
          console.log(user);
          this.toastrService.success('Success');
          this.router.navigateByUrl("home");
        },
        error: (error) => {
          console.log(error);
          this.toastrService.success('Error');
        }
      })
    }
  }



  export class HomeComponent {

  }


  export class NavComponent {
    customerService = inject(CustomerService);

    logout() {
      this.customerService.logout();
    }

  }


  export class TodoListComponent implements OnInit {
      private todoService = inject(TodoService);
      private toastrService = inject(ToastrService);
      todos?:  Todo[] = [];
      model: any = {};
      private router = inject(Router);

      ngOnInit(): void {
        this.list();
      }

      list() {
        this.todoService.list().subscribe({
          next: (todos: any) => {
            this.todos = todos;
            console.log('success');
            console.log(todos);
            this.toastrService.success('Success');
          },
          error: (error) => {
            console.log(error);
            this.toastrService.success('Fail');
          }
        })
      }

      delete(id: number) {
        console.log("id : " + id);
        this.todoService.delete(id).subscribe({
          next: _ => {
            this.todos = this.todos?.filter(todo => todo.id !== id);
            this.toastrService.success('success');
          },
          error: (error) => {
            console.log(error);
            this.toastrService.error(error);
          }
        })
      }

      edit(todo: Todo) {
        this.router.navigate(['todoEdit']);
        this.todoService.sendTodo(todo);
      }
  }


  @Component({
    selector: 'app-todo-register',
    imports: [FormsModule],
    templateUrl: './todo-register.component.html',
    styleUrl: './todo-register.component.css'
  })
  export class TodoRegisterComponent {
      private todoService = inject(TodoService);
      private toastrService = inject(ToastrService);
      private router = inject(Router);
      model: any = {};

      add() {
        this.todoService.add(this.model).subscribe({
          next:() => {
            console.log('success');
            this.toastrService.success('Success');
            this.router.navigateByUrl('todo');
          },
          error: (error) => {
            alert('fail');
            console.log(error);
            this.toastrService.success('Fail');
          }
        })
      }
  }



@Component({
  selector: 'app-todo-detail',
  imports: [FormsModule, CommonModule],
  templateUrl: './todo-detail.component.html',
  styleUrl: './todo-detail.component.css'
})
export class TodoDetailComponent implements OnInit {
    private todoService = inject(TodoService);
    private toastrService = inject(ToastrService);
    todo: any = {};
    private route = inject(ActivatedRoute);

    ngOnInit(): void {
      this.route.paramMap.subscribe(
        params => {
          const idParam = params.get("id");
          if (idParam) {
            this.get(Number(idParam));
          }
        }
      )
    }


    get(id: number) {
      this.todoService.get(id).subscribe({
        next: (todo: any) => {
          this.todo = todo;
          console.log('success');
          this.toastrService.success('Success');
        },
        error: (error) => {
          console.log(error);
          this.toastrService.success('Fail');
        }
      })
    }
}


@Component({
  selector: 'app-todo-edit',
  imports: [FormsModule],
  templateUrl: './todo-edit.component.html',
  styleUrl: './todo-edit.component.css'
})
export class TodoEditComponent implements OnInit, OnDestroy {
  private todoService = inject(TodoService);
  private toastrService = inject(ToastrService);
  private todoSubscription: Subscription | undefined;
  private router = inject(Router);
  todoEdit?: Todo;

  update() {
    if (this.todoEdit === undefined) {
      this.toastrService.error("Todo 정보 오류");
      return;
    }

    this.todoService.update(this.todoEdit).subscribe({
      next: (todo) => {
        this.toastrService.success("success");
        this.router.navigateByUrl('todo');
      },
      error: (error) => {
        this.toastrService.error(error);
      }
    })
  }

  ngOnInit(): void {
    console.log('todoedit');
    this.todoSubscription = this.todoService.currentTodoToEdit.subscribe(
      todo => {
        if (todo) {
          this.todoEdit = todo;
          console.log('받은 todo : ' + this.todoEdit);
        }
      }
    )
  }

  ngOnDestroy(): void {
    if (this.todoSubscription) {
      this.todoSubscription.unsubscribe();
    }
  }
}


11. Interceptor 생성
ng g interceptor _interceptor/auth


  export const authInterceptor: HttpInterceptorFn = (req, next) => {

    var userString = localStorage.getItem("user");

    console.log('authIntercepter...');
    if (!userString) return next(req);

    var user = JSON.parse(userString);
    var token = user.token;

    if (token) {
      req = req.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`
        }
      });
    }
    return next(req);
  };





12. 화면 작성


login.component.html

  <main class="form-signin w-100 m-auto">
    <form #registerForm="ngForm" (ngSubmit)="login()">
      <h1 class="h3 mb-3 fw-normal">로그인</h1>
      <div class="form-floating">
        <input
          type="text"
          name="userName"
          [(ngModel)]="model.userName"
          class="form-control"
          id="floatingInput"
          placeholder="name@example.com"
        />
        <label for="floatingInput">성명</label>
      </div>
      <div class="form-floating">
        <input
          type="password"
          class="form-control"
          name="password"
          [(ngModel)]="model.password"
          id="floatingPassword"
          placeholder="Password"
        />
        <label for="floatingPassword">Password</label>
      </div>
      <div class="form-check text-start my-3">
        <input
          class="form-check-input"
          type="checkbox"
          value="remember-me"
          id="checkDefault"
        />
        <label class="form-check-label" for="checkDefault"> Remember me </label>
      </div>
      <button class="btn btn-primary w-100 py-2" type="submit">Sign in</button>
      <p class="mt-5 mb-3 text-body-secondary">© 2017–2025</p>
    </form>
  </main>


login.component.css

  html,
  body {
    height: 100%;
  }

  .form-signin {
    max-width: 530px;
    padding: 1rem;
  }

  .form-signin .form-floating:focus-within {
    z-index: 2;
  }

  .form-signin input[type="email"] {
    margin-bottom: -1px;
    border-bottom-right-radius: 0;
    border-bottom-left-radius: 0;
  }

  .form-signin input[type="password"] {
    margin-bottom: 10px;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
  }


register.component.html

  <main class="form-signin w-100 m-auto">
    <form #registerForm="ngForm" (ngSubmit)="register()">
      <h1 class="h3 mb-3 fw-normal">회원가입</h1>
      <div class="form-floating">
        <input
          type="text"
          name="userName"
          [(ngModel)]="model.userName"
          class="form-control"
          id="floatingInput"
          placeholder="name@example.com"
        />
        <label for="floatingInput">성명</label>
      </div>
      <div class="form-floating">
        <input
          type="email"
          class="form-control"
          name="email"
          [(ngModel)]="model.email"
          id="floatingEmail"
          placeholder="name@example.com"
        />
        <label for="floatingEmail">Email address</label>
      </div>
      <div class="form-floating">
        <input
          type="password"
          class="form-control"
          name="password"
          [(ngModel)]="model.password"
          id="floatingPassword"
          placeholder="Password"
        />
        <label for="floatingPassword">Password</label>
      </div>
      <div class="form-check text-start my-3">
        <input
          class="form-check-input"
          type="checkbox"
          value="remember-me"
          id="checkDefault"
        />
        <label class="form-check-label" for="checkDefault"> Remember me </label>
      </div>
      <button class="btn btn-primary w-100 py-2" type="submit">Sign in</button>
      <p class="mt-5 mb-3 text-body-secondary">© 2017–2025</p>
    </form>
  </main>


register.component.css

html,
body {
  height: 100%;
}

.form-signin {
  max-width: 530px;
  padding: 1rem;
}

.form-signin .form-floating:focus-within {
  z-index: 2;
}

.form-signin input[type="email"] {
  margin-bottom: -1px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}

.form-signin input[type="password"] {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}


home.component.html

    <main class="container">
      <div class="p-4 p-md-5 mb-4 rounded text-body-emphasis bg-body-secondary">
        <div class="col-lg-6 px-0">
          <h1 class="display-4 fst-italic">Title of a longer featured blog post</h1>
          <p class="lead my-3">
            Multiple lines of text that form the lede,
          </p>
          <p class="lead mb-0">
            <a href="#" class="text-body-emphasis fw-bold">Continue reading...</a>
          </p>
        </div>
      </div>
      <div class="row g-5">
        <div class="col-md-8">
          <h3 class="pb-4 mb-4 fst-italic border-bottom">From the Firehose</h3>
          <article class="blog-post">
            <h2 class="display-5 link-body-emphasis mb-1">Sample blog post</h2>
            <p class="blog-post-meta">January 1, 2021 by <a href="#">Mark</a></p>
            <p>
              This blog post shows a few different types of content that’s supported
              and styled with Bootstrap. Basic typography, lists, tables, images,
              code, and more are all supported as expected.
            </p>
            <hr />
            <p>
              This is some additional paragraph placeholder content.
            </p>
            <pre><code>Example code block</code></pre>
            <p>
              This is some additional paragraph placeholder content. It's a slightly
              shorter version of the other highly repetitive body text used
              throughout.
            </p>
          </article>


          <nav class="blog-pagination" aria-label="Pagination">
            <a class="btn btn-outline-primary rounded-pill" href="#">Older</a>
            <a
              class="btn btn-outline-secondary rounded-pill disabled"
              aria-disabled="true"
              >Newer</a
            >
          </nav>
        </div>
        <div class="col-md-4">
          <div class="position-sticky" style="top: 2rem">
            <div class="p-4 mb-3 bg-body-tertiary rounded">
              <h4 class="fst-italic">About</h4>
              <p class="mb-0">
                Customize this section to tell your visitors a little bit about your
                publication, writers, content, or something else entirely. Totally
                up to you.
              </p>
            </div>
            <div>
              <h4 class="fst-italic">Recent posts</h4>
              <ul class="list-unstyled">
                <li>
                  <a
                    class="d-flex flex-column flex-lg-row gap-3 align-items-start align-items-lg-center py-3 link-body-emphasis text-decoration-none border-top"
                    href="#"
                  >
                    <svg
                      aria-hidden="true"
                      class="bd-placeholder-img"
                      height="96"
                      preserveAspectRatio="xMidYMid slice"
                      width="100%"
                      xmlns="http://www.w3.org/2000/svg"
                    >
                      <rect width="100%" height="100%" fill="#777"></rect>
                    </svg>
                    <div class="col-lg-8">
                      <h6 class="mb-0">Example blog post title</h6>
                      <small class="text-body-secondary">January 15, 2024</small>
                    </div>
                  </a>
                </li>
              </ul>
            </div>

            <div class="p-4">
              <h4 class="fst-italic">Elsewhere</h4>
              <ol class="list-unstyled">
                <li><a href="#">GitHub</a></li>
                <li><a href="#">Social</a></li>
                <li><a href="#">Facebook</a></li>
              </ol>
            </div>
          </div>
        </div>
      </div>
    </main>


nav.component.html

    <div class="container">
      <header class="border-bottom lh-1 py-3">
        <div class="row flex-nowrap justify-content-between align-items-center">

          @if (!customerService.currentUser()) {
            <div class="col-4 pt-1">
              <a class="link-secondary" href="register">회원가입</a>
            </div>
          }
          <div class="col-4 text-center">
            <a
              class="blog-header-logo text-body-emphasis text-decoration-none"
              href="#"
              >Todo 서비스</a
            >
          </div>
          <div class="col-4 d-flex justify-content-end align-items-center">
            <a class="link-secondary" href="#" aria-label="Search">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                width="20"
                height="20"
                fill="none"
                stroke="currentColor"
                stroke-linecap="round"
                stroke-linejoin="round"
                stroke-width="2"
                class="mx-3"
                role="img"
                viewBox="0 0 24 24"
              >
                <title>Search</title>
                <circle cx="10.5" cy="10.5" r="7.5"></circle>
                <path d="M21 21l-5.2-5.2"></path>
              </svg>
            </a>
          @if (!customerService.currentUser()) {
            <a class="btn btn-sm btn-outline-secondary" href="login">로그인</a>
          }
          @else {
            <a href="" (click)="logout()">로그아웃</a>
          }
          </div>
        </div>
      </header>
      <div class="nav-scroller py-1 mb-3 border-bottom">
        <nav class="nav nav-underline justify-content-between">
          <a class="nav-item nav-link link-body-emphasis active" href="todo">Todo</a>
          <a class="nav-item nav-link link-body-emphasis" href="#">U.S.</a>
          <a class="nav-item nav-link link-body-emphasis" href="#">Design</a>
          <a class="nav-item nav-link link-body-emphasis" href="#">Culture</a>
          <a class="nav-item nav-link link-body-emphasis" href="#">Business</a>
          <a class="nav-item nav-link link-body-emphasis" href="#">Politics</a>
          <a class="nav-item nav-link link-body-emphasis" href="#">Opinion</a>
          <a class="nav-item nav-link link-body-emphasis" href="#">Science</a>
          <a class="nav-item nav-link link-body-emphasis" href="#">Health</a>
          <a class="nav-item nav-link link-body-emphasis" href="#">Style</a>
          <a class="nav-item nav-link link-body-emphasis" href="#">Travel</a>
        </nav>
      </div>
    </div>


todo-detail.component.html

  <div class="container mt-5">
    <div class="card shadow-sm">
      <div class="card-header bg-primary text-white">
        <h2 class="mb-0">Todo Details</h2>
      </div>
      <div class="card-body">
        @if (todo) {
        <h3 class="card-title">{{ todo.title }}</h3>
        <p class="card-text">
          <strong>Description:</strong> {{ todo.description }}
        </p>
        <p class="card-text">
          <strong>Status:</strong>
          <span
            class="badge"
            [ngClass]="todo.isCompleted ? 'bg-success' : 'bg-danger'"
          >
            {{ todo.isCompleted ? "Completed" : "Pending" }}
          </span>
        </p>
        <hr />
        <div class="d-flex justify-content-between">
          <a href="todo" class="btn btn-secondary">Back to List</a>
        </div>
        }
      </div>
    </div>
  </div>


todo-edit.component.html

  <div class="container mt-5">
      <div class="row justify-content-center">
        @if (todoEdit) {
          <div class="col-md-8 col-lg-6">
              <div class="card">
                  <div class="card-header">
                      <h3 class="card-title text-center">Edit Todo</h3>
                  </div>
                  <div class="card-body">
                      <form #registerTodo="ngForm" (ngSubmit)="update()">
                          <div class="mb-3">
                              <label for="todoTitle" class="form-label">Title</label>
                              <input type="text" class="form-control" [(ngModel)]="todoEdit.title" name="title" required>
                          </div>
                          <div class="mb-3">
                              <label for="todoDescription" class="form-label">Description</label>
                              <textarea class="form-control" [(ngModel)]="todoEdit.description" name="description" rows="3"></textarea>
                          </div>
                          <div class="mb-3 form-check">
                              <input type="checkbox" class="form-check-input" [(ngModel)]="todoEdit.isCompleted"  name="isCompleted">
                              <label class="form-check-label" for="todoIsCompleted">Is Completed?</label>
                          </div>
                          <div class="d-grid gap-2">
                              <button type="submit" class="btn btn-primary" >Register Todo</button>
                          </div>
                      </form>
                  </div>
              </div>
          </div>
        }
      </div>
  </div>


todo-list.component.html

    <div class="container mt-5">
        <div class="d-flex justify-content-between align-items-center mb-4">
            <h2>Todo List</h2>
            <a href="todoAdd" class="btn btn-primary">Add New Todo</a>
        </div>
        ---
        <div class="table-responsive">
            <table class="table table-striped table-hover">
                <thead>
                    <tr>
                        <th scope="col">#</th>
                        <th scope="col">Title</th>
                        <th scope="col">Description</th>
                        <th scope="col">Completed</th>
                        <th scope="col">Actions</th>
                    </tr>
                </thead>
                <tbody id="todoTableBody">
                  @for (item of todos; track item.id) {
                    <tr>
                        <td>{{item.id}}</td>
                        <td>{{item.title}}</td>
                        <td>{{item.description}}</td>
                        <td><span class="badge bg-success">{{item.isCompleted}}</span></td>
                        <td>
                            <a class="btn btn-info btn-sm" href="todoDetail/{{item.id}}">View</a>
                            <button class="btn btn-warning btn-sm" (click)="edit(item)" >Edit</button>
                            <button class="btn btn-danger btn-sm" (click)="delete(item.id)">Delete</button>
                        </td>
                    </tr>
                    }
                </tbody>
            </table>
        </div>
        <p class="text-center mt-3" id="noTodosMessage" style="display: none;">No Todo items found.</p>
    </div>


todo-register.component.html


    <div class="container mt-5">
        <div class="row justify-content-center">
            <div class="col-md-8 col-lg-6">
                <div class="card">
                    <div class="card-header">
                        <h3 class="card-title text-center">Register New Todo</h3>
                    </div>
                    <div class="card-body">
                        <form #registerTodo="ngForm" (ngSubmit)="add()">
                            <div class="mb-3">
                                <label for="todoTitle" class="form-label">Title</label>
                                <input type="text" class="form-control" [(ngModel)]="model.title" name="title" required>
                            </div>
                            <div class="mb-3">
                                <label for="todoDescription" class="form-label">Description</label>
                                <textarea class="form-control" [(ngModel)]="model.description" name="description" rows="3"></textarea>
                            </div>
                            <div class="mb-3 form-check">
                                <input type="checkbox" class="form-check-input" [(ngModel)]="model.isCompleted"  name="isCompleted">
                                <label class="form-check-label" for="todoIsCompleted">Is Completed?</label>
                            </div>
                            <div class="d-grid gap-2">
                                <button type="submit" class="btn btn-primary" >Register Todo</button>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>




댓글 쓰기

댓글 목록