오늘은 필자가 사용한 api에 대해 정리해보도록 하겠다. 필자는 api를 캘린더 페이지에서 약 등록 시 정보를 불러올 때, 약 등록 시 알림 구독을 했다면 메일을 보여줄 때와, 마이페이지에서 전체 복약 목록을 보여줄 때 불러왔다.

초반에는 캘린더페이지 내에서 사이드바에 전체 복약 정보를 보여주도록 하였지만, 디자이너님의 마이페이지 내에서 필요 이상의 모달이 존재한다는 피드백으로 마이페이지로 이동하게 되었다.

이미 다 api폴더 세팅 및 디자인을 해놓은 상태여서 변경을 하는 게 번거롭기도 하였지만 ux를 생각하면 분명 옮기는 것이 맡는 판단이긴 했다.

프로젝트를 제작하며 분명 갈등 아닌 갈등이 생기기도 하지만 본인이나 프로젝트 구성원 타인의 편의성이 아닌 이 제품을 사용할 사용자의 편의성을 1순위로 생각해야 한다는 마인드로 제작을 하다보면 ,, 어찌저찌 만들어진다.

필자는 api 폴더 구분을 명확하게 하기 위해 같은 약 api를 불러옴에도 불구하고 api 폴더를 마이페이지 영역과 캘런더 영역 2개로 나누었지만, 코드의 중복성 문제가 있다고 생각한다. 하지만 .. api가 꼬이는 것 보다는 코드가 중복되는 게 차선이라고 생각하여 분리해두었다.


🟡 캘런더 페이지 내 api

폴더 내 파일 구조는 위와 같다. 다른 폴더는 캘린더페이지를 제작하신 분이 사용하시기 때문에 medi 폴더만 보면 된다.


🟡 src\app\api\calendar\medi\all\route.ts

import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const supabase = createClient(supabaseUrl, supabaseKey);

export async function GET() {
  try {
    const { data, error } = await supabase
      .from('medications')
      .select('id, medi_name, times, notes, created_at');
    if (error) {
      console.error('Error fetching medication records:', error);
      return NextResponse.json({ error: 'Failed to fetch medication records' }, { status: 500 });
    }
    return NextResponse.json({ medicationRecords: data });
  } catch (error) {
    console.error('Error fetching medication records:', error);
    return NextResponse.json({ error: 'Failed to fetch medication records' }, { status: 500 });
  }
}

GET 메서드를 처리하는 API 엔드포인트. 수파베이스에 있는 medications 테이블에서 데이터를 가져오는 역할을 하는 코드다.

한 줄 한 줄 뜯어보도록 하겠다.


import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';

next에서 제공하는 객체를 임포트해준다. api 요청과 응답을 다루는 객체. http 요청을 처리하고 클라이언트로 적절한 응답을 반환하여준다.

서버가 클라이언트에게 요청을 보낼 때 사용한다.

createClient는 수파베이스와 상호작용할 수 있는 클라이언트를 생성하는 함수이다.


const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const supabase = createClient(supabaseUrl, supabaseKey);

환경변수 값을 가져와 데이터베이스의 url과 api에 접근할 수 있는 키를 설정. 수파베이스 공식 문서에도 있기 때문에 패스하겠다.


export async function GET() {
  try {
    const { data, error } = await supabase
      .from('medications')
      .select('id, medi_name, times, notes, created_at');
    if (error) {
      console.error('Error fetching medication records:', error);
       return NextResponse.json({ error: 'Failed to fetch medication records' }, { status: 500 });
    }
    return NextResponse.json({ medicationRecords: data });
  } catch (error) {
    console.error('Error fetching medication records:', error);
    return NextResponse.json({ error: 'Failed to fetch medication records' }, { status: 500 });
  }
}
  • 🟣 RESTful API란?
  • HTTP 메서드를 사용한 API

  • 자원(Resource)을 URL로 표현하고, HTTP 메서드를 통해 해당 자원에 대해 다양한 작업을 수행


http get 요청을 처리하는 api 핸들러. 데이터를 조회한다.

medicaitons 테이블에 접근하여 select 내에 있는 필드 값을 선택하여 데이터를 가져온다.

async는 비동기 작업을 처리하기 위한 예약어로, 비동기 요청(예: API 호출)을 기다리는 데 사용

await는 비동기 함수로, 데이터를 불러오는 작업이 완료될 때까지 기다린다. -> 작업이 완료되기 전까지 다른 코드가 실행되지 않도혹 보장하기 위해서!

data에 약물 정보를 저장하고, error에는 데이터 조회 중 오류가 발생하면 이 변수에 저장한다.

오류 발생 시, json 형식으로 오류메세지를 반환하고 상태코드를 500으로 설정하여 응답을 보낸다.

오류가 없다면 db에서 가져온 정보를 클라이언트에게 json 형식으로 반환한다.


🟡 src\app\api\calendar\medi\names\route.ts

import { NextResponse } from 'next/server';

export async function GET(req: Request) {
  try {
    const apiUrl = `${process.env.NEXT_PUBLIC_E_MEDI_URL}?serviceKey=${process.env.NEXT_PUBLIC_E_MEDI_KEY}&pageNo=1&numOfRows=100&type=json`;
    console.log("API URL:", apiUrl);

    const response = await fetch(apiUrl);

    if (!response.ok) {
      console.error("API response not ok:", response.statusText);
      throw new Error("네트워크 응답에 문제가 있습니다.");
    }

    const data = await response.json();
    const items = data.body.items || [];

    const medicineNames = items.map((item: any) => ({
      itemName: item.itemName,
    }));

    return NextResponse.json(medicineNames);
  } catch (error) {
    console.error("Error fetching medicine names:", error);
    return NextResponse.json(
      { error: "데이터를 가져오는 데 실패했습니다." },
      { status: 500 }
    );
  }
}

약 이름 리스트를 불러오는 api


export async function GET(req: Request) {
  try {
    const apiUrl = `${process.env.NEXT_PUBLIC_E_MEDI_URL}?serviceKey=${process.env.NEXT_PUBLIC_E_MEDI_KEY}&pageNo=1&numOfRows=100&type=json`;
    console.log("API URL:", apiUrl);

req: Request는 클라이언트로부터의 HTTP 요청 객체

환경 변수에서 api의 기본 url과 인증을 위한 api키를 환경변수에서 가져온뒤, 첫 번째 페이지의 100개 결과를 json으로 반환 (약 데이터가 너무 많기 때문)


  • 🟣 위 코드에선 req: Request가 없었는데 현 코드에선 있는 이유?
  • req: Request가 포함된 코드는 클라이언트로부터 요청된 데이터를 처리해야하는 경우에 사용. 없는 경우엔 단순한 서버 응답만 필요.

  • 위 코드에선 db에서 테이블의 특정 필드들을 조회하고, 그 결과를 바로 응답을 반환하지만 클라이언트가 요청할 때 별도의 정보를 제공할 필요가 없기 때문에 위 객체가 필요하지 않음.

  • 하지만 현 코드에선 외부 api에 요청을 보내기 위해 위 객체가 필요함. fetch를 사용해 외부 api에 요청을 보내고, 응답을 처리하는 과정에서 req 객체는 클라이언트로부터 필요한 정보를 받아오는데 사용될 수 있음.


const response = await fetch(apiUrl);

    if (!response.ok) {
      console.error("API response not ok:", response.statusText);
      throw new Error("네트워크 응답에 문제가 있습니다.");
    }

response.ok : 상태 코드가 200-299 범위 내에 있는 경우 true를 반환


const data = await response.json();
    const items = data.body.items || [];

    const medicineNames = items.map((item: any) => ({
      itemName: item.itemName,
    }));

API로부터 받은 응답을 JSON 형식으로 변환하여 data에 저장

data.body.items가 존재하면 이를 items 변수에 저장하고, 존재하지 않으면 빈 배열 []을 할당

items 배열의 각 요소에서 itemName 속성만 추출하여 새로운 배열인 medicineNames를 만듦. 이 배열에는 약물의 이름만 포함

map은 원본 배열은 반환하지 않고, 변형된 새로운 배열을 만들어준다.

any 타입을 사용하므로서 외부 api로부터 어떤 형식의 데이터가 올 지 명확하지 않을 때 사용.


    return NextResponse.json(medicineNames);
  } catch (error) {
    console.error("Error fetching medicine names:", error);
    return NextResponse.json(
      { error: "데이터를 가져오는 데 실패했습니다." },
      { status: 500 }
    );
  }
}

medicineNames 배열을 JSON 형식으로 클라이언트에게 응답으로 반환. (json 형식으로 반환하는 이유는 외부 api에서 json 형식의 데이터를 반환하기 때뭉네 이를 js 객체로 변환해서 사용해야함.)

try 블록에서 발생한 오류를 처리하기 위한 블록.


🟡 src\app\api\calendar\medi\route.ts

import { NextRequest, NextResponse } from 'next/server';
import { createClient, PostgrestError } from '@supabase/supabase-js';
import '@/utils/scheduleEmail'; // 스케줄링 로직을 불러옵니다.

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const supabase = createClient(supabaseUrl, supabaseKey);

interface MediRecord {
  id: string;
  medi_name: string;
  medi_nickname: string;
  times: {
    morning: boolean;
    afternoon: boolean;
    evening: boolean;
  };
  notes: string;
  start_date: string;
  end_date: string;
  created_at: string;
  user_id: string;
  day_of_week: string[];
  notification_time: string[];
  repeat: boolean;
}

function isPostgrestError(error: any): error is PostgrestError {
  return error && typeof error === 'object' && 'message' in error && 'details' in error;
}

export async function GET(req: NextRequest) {
  try {
    const { searchParams } = new URL(req.url);
    const user_id = searchParams.get('user_id');

    if (!user_id) {
      console.error('GET request missing user_id parameter');
      return NextResponse.json({ error: 'User ID is required' }, { status: 400 });
    }

    const { data, error } = await supabase
      .from('medications')
      .select('*')
      .eq('user_id', user_id);

    if (error) {
      console.error("Supabase error:", error);
      return NextResponse.json({ error: error.message }, { status: 500 });
    }

    return NextResponse.json({ medicationRecords: data }, { status: 200 });
  } catch (err: unknown) {
    console.error("Server error:", err);
    return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
  }
}

export async function POST(req: NextRequest) {
  try {
    const newMediRecord: MediRecord = await req.json();

    const { data, error } = await supabase
      .from('medications')
      .insert([newMediRecord]);

    if (error) {
      console.error("Supabase error:", error);
      return NextResponse.json({ error: error.message }, { status: 500 });
    }

    return NextResponse.json({ medicationRecords: data }, { status: 201 });
  } catch (err: unknown) {
    console.error("Server error:", err instanceof Error ? err.message : 'Unknown error');
    return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
  }
}

export async function PUT(req: NextRequest) {
  try {
    const { searchParams } = new URL(req.url);
    const id = searchParams.get('id');
    if (!id) {
      console.error('PUT request missing id parameter');
      return NextResponse.json({ error: 'ID is required' }, { status: 400 });
    }

    const updatedMediRecord: MediRecord = await req.json();

    const { data, error } = await supabase
      .from('medications')
      .update(updatedMediRecord)
      .eq('id', id);

    if (error) {
      console.error("Supabase error:", error);
      return NextResponse.json({ error: error.message }, { status: 500 });
    }

    return NextResponse.json({ medicationRecords: data }, { status: 200 });
  } catch (err: unknown) {
    console.error("Server error:", err instanceof Error ? err.message : 'Unknown error');
    return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
  }
}

export async function DELETE(req: NextRequest) {
  try {
    const { searchParams } = new URL(req.url);
    const id = searchParams.get('id');
    if (!id) {
      console.error('DELETE request missing id parameter');
      return NextResponse.json({ error: 'ID is required' }, { status: 400 });
    }

    // 먼저 calendar_medicine 테이블에서 참조된 레코드 삭제
    const { error: bridgeDeleteError } = await supabase
      .from('calendar_medicine')
      .delete()
      .eq('medicine_id', id);

    if (bridgeDeleteError) {
      console.error("Supabase error while deleting from calendar_medicine:", bridgeDeleteError);
      return NextResponse.json({ error: bridgeDeleteError.message }, { status: 500 });
    }

    // 이제 medications 테이블에서 약물을 삭제
    const { data, error } = await supabase
      .from('medications')
      .delete()
      .eq('id', id);

    if (error) {
      console.error("Supabase error while deleting from medications:", error);
      return NextResponse.json({ error: error.message }, { status: 500 });
    }

    return NextResponse.json({ medicationRecords: data }, { status: 200 });
  } catch (err: unknown) {
    console.error("Server error:", err instanceof Error ? err.message : 'Unknown error');
    return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
  }
}

import { NextRequest, NextResponse } from 'next/server';
import { createClient, PostgrestError } from '@supabase/supabase-js';
import '@/utils/scheduleEmail'; // 스케줄링 로직을 불러옵니다.

createClient는 오픈 소스 백엔드 서비스.

PostgrestError는 수파베이스에서 발생하는 오류를 다루기 위한 인터페이스.

그리고 스케쥴링 관련 로직을 포함한 유틸리티 파일을 불러온다. ( 다음 편에 설명 예정)


const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const supabase = createClient(supabaseUrl, supabaseKey);

이전 수파베이스 urlr과 키를 가져와 클라이언트 생성.


interface MediRecord {
  id: string;
  medi_name: string;
  medi_nickname: string;
  times: {
    morning: boolean;
    afternoon: boolean;
    evening: boolean;
  };
  notes: string;
  start_date: string;
  end_date: string;
  created_at: string;
  user_id: string;
  day_of_week: string[];
  notification_time: string[];
  repeat: boolean;
}

여러 파일에서 얘를 사용했는데 타입 불일치의 문제로 참 에러가 많아서 고생했었다. 하하

약 구조를 정의하는 인터페이스. ts를 사용했기 때문에 데이터의 형태를 강제로 주어 안정성을 높혔다.


function isPostgrestError(error: any): error is PostgrestError {
  return error && typeof error === 'object' && 'message' in error && 'details' in error;
}

오류를 확인하는 타입 가드 함수.


export async function GET(req: NextRequest) {
  try {
    const { searchParams } = new URL(req.url);
    const user_id = searchParams.get('user_id');

    if (!user_id) {
      console.error('GET request missing user_id parameter');
      return NextResponse.json({ error: 'User ID is required' }, { status: 400 });
    }

    const { data, error } = await supabase
      .from('medications')
      .select('*')
      .eq('user_id', user_id);

    if (error) {
      console.error("Supabase error:", error);
      return NextResponse.json({ error: error.message }, { status: 500 });
    }

    return NextResponse.json({ medicationRecords: data }, { status: 200 });
  } catch (err: unknown) {
    console.error("Server error:", err);
    return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
  }
}

클라이언트가 보낸 get 요청을 처리.

요청 url에서 id값을 추출하여 id가 없다면 400 코드와 함께 오류 메세지를 반환한다.

medications 테이블에서 user_id에 해당하는 모든 레코드를 조회한다. ( 캘린더 페이지는 로그인 시에만 접근이 가능하며, 로그인 한 사용자가 복용 중인 약물을 보여줘야하기 때문)

오류가 발생하면 500 에러와 함께 오류 메세지를 반환한다.

성공 시에는 200 코드 반환.


export async function POST(req: NextRequest) {
  try {
    const newMediRecord: MediRecord = await req.json();

    const { data, error } = await supabase
      .from('medications')
      .insert([newMediRecord]);

    if (error) {
      console.error("Supabase error:", error);
      return NextResponse.json({ error: error.messa```
ge }, { status: 500 });
    }
    return NextResponse.json({ medicationRecords: data }, { status: 201 });
  } catch (err: unknown) {
    console.error("Server error:", err instanceof Error ? err.message : 'Unknown error');
    return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
  }
}

클라이언트가 보낸 post 요청 처리.

약 기록을 json으로 파싱한 후 db에 기록을 삽입.

오류 발생 시 500, 성공 시 201 코드 반환 후 삽입 데이터를 반환한다.


export async function PUT(req: NextRequest) {
  try {
    const { searchParams } = new URL(req.url);
    const id = searchParams.get('id');
    if (!id) {
      console.error('PUT request missing id parameter');
      return NextResponse.json({ error: 'ID is required' }, { status: 400 });
    }

    const updatedMediRecord: MediRecord = await req.json();

    const { data, error } = await supabase
      .from('medications')
      .update(updatedMediRecord)
      .eq('id', id);

    if (error) {
      console.error("Supabase error:", error);
      return NextResponse.json({ error: error.message }, { status: 500 });
    }

    return NextResponse.json({ medicationRecords: data }, { status: 200 });
  } catch (err: unknown) {
    console.error("Server error:", err instanceof Error ? err.message : 'Unknown error');
    return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
  }
}

클라이언트가 보낸 put 요청 처리.

요청 url에서 id를 추출한 후, (id가 없는 경우엔 400 코드 반환)

수정할 약을 json 형식으로 파싱 후 db에서 해당 id의 기록을 업데이트.


export async function DELETE(req: NextRequest) {
  try {
    const { searchParams } = new URL(req.url);
    const id = searchParams.get('id');
    if (!id) {
      console.error('DELETE request missing id parameter');
      return NextResponse.json({ error: 'ID is required' }, { status: 400 });
    }

    // 먼저 calendar_medicine 테이블에서 참조된 레코드 삭제
    const { error: bridgeDeleteError } = await supabase
      .from('calendar_medicine')
      .delete()
      .eq('medicine_id', id);

    if (bridgeDeleteError) {
      console.error("Supabase error while deleting from calendar_medicine:", bridgeDeleteError);
      return NextResponse.json({ error: bridgeDeleteError.message }, { status: 500 });
    }

    // 이제 medications 테이블에서 약물을 삭제
    const { data, error } = await supabase
      .from('medications')
      .delete()
      .eq('id', id);

    if (error) {
      console.error("Supabase error while deleting from medications:", error);
      return NextResponse.json({ error: error.message }, { status: 500 });
    }

    return NextResponse.json({ medicationRecords: data }, { status: 200 });
  } catch (err: unknown) {
    console.error("Server error:", err instanceof Error ? err.message : 'Unknown error');
    return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
  }
}

클라이언트가 보낸 삭제 요청 처리.

calendar_medicine 테이블에서 해당 약물이 참조된 레코드를 삭제 후, medications 테이블에서 해당 약물을 삭제


profile
프론트엔드 개발자를 향해서

0개의 댓글

Powered by GraphCDN, the GraphQL CDN