angular

supabase crud ( angular를 사용하여 memo-app 만들기 )

호리둥절 2023. 4. 11. 17:07

이전에 supabase auth에 대해 구현해 보았습니다. 

https://develop-const.tistory.com/17

 

supabase auth ( angular를 사용하여 회원가입, 로그인, 로그아웃 구현하기 )

https://supabase.com/docs/guides/auth Auth | Supabase Docs Need some help? Not to worry, our specialist engineers are here to help. Submit a support ticket through the Dashboard. supabase.com 이전에 supabase를 알아보는 시간에 프로젝트를 만

develop-const.tistory.com

 

오늘은 supabase를 이용해 간단한 메모앱을 만들어 보겠습니다. 🔥🔥

💝 memos 테이블 만들기

table editer에서 new table을 클릭 후 table Name에 memos를 입력해주고 column에 아래와 같이 name과 type을 입력해줍니다.

저처럼 이렇게 supabase에서 제공해주는 ui툴을 사용하여 테이블을 만들어도 되지만, sql문법을 사용하시는것이 익숙하신분은 sql editer에서 new query를 클릭해서 직접 쿼리를 입력하셔도 됩니다.

 

memos 테이블에서 user_id 외래키로 가져오기

select a table to reference to에서 users테이블을 클릭한후 user_id를 외래키로 가져온후 save 해줍니다.

💛 memo.service.ts 생성하기

ng generate service memo --skip-tests=true
import { Injectable } from '@angular/core';
import { BehaviorSubject } from "rxjs";

import { SupabaseService } from './supabase.service';
import { IMemo } from 'src/interface/memo';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root'
})
export class MemoService {
  constructor(private readonly supabase: SupabaseService, private readonly authService: AuthService) {
  }

  // 메모 생성
  async createMemo(memo: IMemo) {
    const userId = await this.authService.getProfile()

    return await this.supabase.getSupabase().from('memos')
      .insert({ user_id: userId, ...memo })
  }

  // 오늘 작성한 메모 가져오기
  async getMemos() {
    const now = new Date();
    const timestamp = now.toISOString().slice(0, 10); // ISO 8601 문자열로 변환

    const userId = await this.authService.getProfile()

    const { data, error } = await this.supabase.getSupabase().from('memos')
      .select('*')
      .eq('user_id', userId)
      .eq('created_at', timestamp)

    if (error) {
      return;
    }

    return data.map((memo: any) => memo as IMemo);
  }

  // 특정 메모 가져오기
  async getMemo(id: number) {
    const userId = await this.authService.getProfile()

    const { data, error } = await this.supabase.getSupabase().from('memos')
      .select('*')
      .eq('user_id', userId)
      .eq('id', id)
      .single();

    if (error) {
      return;
    }

    return data as IMemo;
  }

  // 메모 업데이트
  async updateMemo(id: number, memo: IMemo) {
    return await this.supabase.getSupabase().from('memos')
      .update({ ...memo })
      .eq('id', id)
      .single();
  }

  // 메모 삭제
  async deleteMemo(id: number) {
    return await this.supabase.getSupabase().from('memos')
      .delete()
      .eq('id', id)
      .single();
  }

}
  • createMemo(memo: IMemo) - 메모를 생성하는 함수입니다. 사용자 ID와 함께 받은 memo 객체를 데이터베이스의 'memos' 테이블에 삽입합니다.
  • getMemos() - 사용자의 모든 메모를 가져오는 함수입니다. 현재 날짜의 ISO 8601 문자열을 생성한 후, 데이터베이스에서 해당 사용자 ID와 생성 날짜가 일치하는 모든 메모를 선택하여 반환합니다.
  • getMemo(id: number) - 특정 메모를 가져오는 함수입니다. 받은 id에 해당하는 메모를 데이터베이스에서 조회하고, 사용자 ID와 일치하는 경우 해당 메모를 반환합니다.
  • updateMemo(id: number, memo: IMemo) - 메모를 업데이트하는 함수입니다. 받은 id에 해당하는 메모를 데이터베이스에서 찾아서, memo 객체의 내용으로 업데이트합니다.
  • deleteMemo(id: number) - 메모를 삭제하는 함수입니다. 받은 id에 해당하는 메모를 데이터베이스에서 삭제합니다.

이 클래스의 모든 함수는 비동기(async)로 선언되어 있으며, Supabase 클라이언트를 사용하여 데이터베이스와 통신합니다. 각 함수에서는 Supabase 클라이언트의 메서드를 사용해 데이터베이스에 쿼리를 실행하고, 비동기 처리를 통해 작업이 완료될 때까지 기다립니다. 작업이 성공적으로 완료되면 결과를 반환하며, 오류가 발생하면 처리합니다.

💚 memo.page.html 과 memo.page.ts 생성하기

cli를 통해 html파일과 ts파일을 standalone으로 생성해보겠습니다.

ng g c memo --type page --inline-style --skip-tests --standalone

 

tailwind css와 daisyui를 이용해 간단하게 html 코드를 생성해보겠습니다.

<div class="w-full flex justify-end px-2 py-6">
  <button (click)="onLogout()" class="btn w-32">로그아웃</button>
</div>
<div class="w-full h-screen flex justify-center items-center">
  <div class="w-1/5 h-1/2 rounded-xl shadow-md text-center px-4">
    <div class="h-[460px]">
      <p class="text-xl font-bold  pt-4">오늘의 할일</p>
      <div class="h-[410px] py-4 space-y-2 overflow-y-scroll">
        <div *ngFor="let memo of memos$">
          <div class="justify-between border w-full rounded-md text-start flex items-center px-4">
            <div class="form-control py-2">
              <p class="pl-2">{{memo.title}}</p>
            </div>
            <div>
              <i class="fa-regular fa-pen-to-square pr-2" [routerLink]="['/memo-detail', memo.id]"></i>
              <i class="fa-solid fa-xmark" (click)="onDeleteMemo(memo.id)"></i>
            </div>
          </div>
        </div>
      </div>
      <button [routerLink]="['/memo-detail']" class="btn w-full">입력하기</button>
    </div>
  </div>
</div>
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, RouterLink } from '@angular/router';

import { Observable } from 'rxjs';

import { UserService } from 'src/services/user.service';
import { MemoService } from 'src/services/memo.service';
import { IMemo } from 'src/interface/memo';
import { AuthService } from 'src/services/auth.service';

@Component({
  selector: 'app-memo',
  standalone: true,
  imports: [CommonModule, RouterLink],
  templateUrl: './memo.page.html',
  styles: [
  ]
})
export class MemoPage {
  memos$?: IMemo[] // 메모들을 저장할 memos$ 변수 선언 (IMemo 타입 배열)

  // 클래스 생성자에 필요한 서비스와 라우터 인스턴스 주입
  constructor(private readonly auth: AuthService,private readonly userService: UserService, private readonly memoService: MemoService, private readonly router: Router) { }

  // ngOnInit 메서드 실행 시 메모를 가져옴
  async ngOnInit() {
    this.getMemos() // 메모 목록 가져오기
    this.auth.getSession() // 사용자 세션 가져오기
  }

  // 로그아웃 버튼 클릭 시 로그아웃 처리
  async onLogout() {
    const { error } = await this.userService.signOut(); // 로그아웃 처리

    if (error) { // 오류가 발생한 경우, 반환
      return
    }

    this.router.navigate(['/login']) // 로그인 페이지로 이동
  }

  // 메모 목록 가져오기
  async getMemos() {
    this.memos$ = await this.memoService.getMemos() // 메모 목록을 가져와서 memos$에 저장
  }

  // 메모 삭제 처리
  async onDeleteMemo(id: number) {
    const { error } = await this.memoService.deleteMemo(id) // 메모 삭제

    if (error) { // 오류가 발생한 경우, 반환
      return
    }

    this.getMemos(); // 메모 목록 새로고침
  }
}

 

설명 주석 참조※

💙 memo-detail.page.html 와 memo-detail.page.ts 생성하기

ng g c memo-detail --type page --inline-style --skip-tests --standalone
<div class="flex items-center justify-center w-full h-screen">
  <div class="w-1/5 px-4 text-center shadow-md h-1/2 rounded-xl">
    <div class="h-[460px]" [formGroup]="memoForm!">
      <p class="pt-4 text-xl font-bold">할일을 입력해보세요!</p>
      <input class="w-full my-4 input input-bordered" formControlName="title" placeholder="제목" />
      <textarea class="textarea textarea-bordered w-full h-[300px]" formControlName="content"
        placeholder="내용"></textarea>
    </div>
    <button class="w-full btn" (click)="!this.id?  onCreateMemo():onUpdateMemo()">
      {{!this.id? '추가하기':'수정하기'}}
    </button>
  </div>
</div>
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { ReactiveFormsModule, FormGroup, FormBuilder, Validators } from '@angular/forms';

import { UserService } from 'src/services/user.service';
import { MemoService } from 'src/services/memo.service';
import { IMemo } from 'src/interface/memo';

@Component({
  selector: 'app-memo-detail',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  templateUrl: './memo-detail.page.html',
  styles: [
  ]
})
export class MemoDetailPage {
  id?: number; // 메모 ID를 저장할 변수 선언
  memo$?: IMemo // 메모를 저장할 변수 선언 (IMemo 타입)
  memoForm?: FormGroup; // 메모 폼을 저장할 변수 선언 (FormGroup 타입)

  // 클래스 생성자에 필요한 FormBuilder, 메모 서비스, 라우터, ActivatedRoute 인스턴스 주입
  constructor(private fb: FormBuilder, private readonly memoService: MemoService, private readonly router: Router, public route: ActivatedRoute) {
    // 메모 폼 초기화
    this.memoForm = this.fb.group({
      title: ['', Validators.required], // 필수 입력 필드
      content: ['', Validators.required], // 필수 입력 필드
      is_success: [false],
      created_at: [new Date()]
    });
  }

  // ngOnInit 메서드 실행 시 메모 가져옴
  ngOnInit() {
    this.route.params.subscribe(async params => {
      this.id = params['id']; // 라우팅 파라미터에서 메모 ID 가져옴
      this.memo$ = await this.memoService.getMemo(this.id!) // 메모 가져오기
      this.memoForm?.patchValue({ ...this.memo$ }) // 가져온 메모의 값을 폼에 적용
    });
  }

  // 메모 생성 버튼 클릭 시 메모 생성 처리
  async onCreateMemo() {
    const { error } = await this.memoService.createMemo(this.memoForm?.value) // 메모 생성

    if (error) { // 오류가 발생한 경우, 반환
      return
    }

    this.router.navigate(['/memo']) // 메모 페이지로 이동
  }

  // 메모 수정 버튼 클릭 시 메모 수정 처리
  async onUpdateMemo() {
    const { error } = await this.memoService.updateMemo(this.id!, this.memoForm?.value) // 메모 수정

    if (error) { // 오류가 발생한 경우, 반환
      return
    }

    this.router.navigate(['/memo']) // 메모 페이지로 이동
  }
}

설명 주석 참조

 

🏸 gaurd 생성하기

가드는 라우팅을 처리하기 전에 실행되어 라우트 접근을 허용할지 막을지 결정하는 역할을 합니다.

 

auth.guard.ts

로그인되어있지 않은 상태에서 memo 페이지 접근시 login페이지로 이동

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from 'src/services/auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {

  constructor(private authService: AuthService, private router: Router) { }
    // canActivate 메서드는 라우트 가드로 사용되며, 사용자가 특정 라우트에 접근할 수 있는지 확인하는 역할을 합니다.
    async canActivate() {
      // authService의 getSession 메서드를 호출하여 사용자의 세션 정보를 가져옵니다.
      const session = await this.authService.getSession();
      
      // 세션 정보가 없으면 (즉, 사용자가 로그인하지 않은 상태라면)
      if (!session) {
        // 로그인 페이지로 리다이렉션합니다.
        this.router.navigate(['/login']);
        
        // false를 반환하여 현재 라우트 접근을 막습니다.
        return false;
      }
      
      // 세션 정보가 존재하면 (즉, 사용자가 로그인한 상태라면)
      // true를 반환하여 라우트 접근을 허용합니다.
      return true;
    }
}

login.guard.ts

로그인되어있는상태에서 login페이지 접근시 memo페이지로 이동

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from 'src/services/auth.service';
@Injectable({
  providedIn: 'root'
})
export class LoginAuthGuard implements CanActivate {

  constructor(private authService: AuthService, private router: Router) { }

  async canActivate() {
    const session = await this.authService.getSession();
    // 세션 정보가 존재하면 (즉, 사용자가 로그인한 상태라면)
    if (session) {
      // 메모 페이지로 리다이렉션합니다.
      this.router.navigate(['/memo']);

      // false를 반환하여 현재 라우트 접근을 막습니다.
      return false;
    }

    // 세션 정보가 없으면 (즉, 사용자가 로그인하지 않은 상태라면)
    // true를 반환하여 라우트 접근을 허용합니다.
    return true;
  }
}

결과화면

생성, 수정, 삭제 등이 잘 되는것을 보실수있습니다. 😀😀

 

더 자세한 것을 알고싶다면 아래의 supabase docs에서 확인해보시길 바랍니다. 😀

https://supabase.com/docs

 

Supabase Docs

Supabase is an open source Firebase alternative providing all the backend features you need to build a product. Learn more about Supabase, follow a quickstart for an overview, or dive straight into the different products and APIs.

supabase.com