-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/#29 supabase api #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
90d836d
a76d70b
3cea676
71b14b6
c8125e6
5e3a3cc
56f82bc
a58f486
dd00467
1be8b47
4ed7db0
6e56bd9
e68d8c1
62d24ec
646d1c3
ebb04b8
49ba716
1c365c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| 'use server'; | ||
|
|
||
| import { camelToSnakeObject, snakeToCamelObject } from '@/lib/utils/camelize.util'; | ||
| import { getServerClient } from '@/lib/utils/supabase/server.util'; | ||
| import { | ||
| CreateMealDetailDTO, | ||
| CreateMealDTO, | ||
| MealDetailDTO, | ||
| MealDetailSnakeCaseDTO, | ||
| MealDTO, | ||
| MealOverviewDTO, | ||
| MealOverviewSnakeCaseDTO, | ||
| MealSnakeCaseDTO | ||
| } from '@/types/DTO/meal.dto'; | ||
|
|
||
| /** | ||
| * 특정 기간 내의 사용자 식사 기록을 모든 상세 정보와 함께 조회한다. | ||
| * | ||
| * @param {string} startDate - 조회 시작 날짜 ex) 2023-10-15 | ||
| * @param {string} endDate - 조회 종료 날짜 ex) 2023-10-15 | ||
| * @returns {MealDTO[]} 사용자의 식사 정보 배열 (MealDTO[]) | ||
| * @throws Supabase 쿼리 실행 중 오류가 발생한 경우 Error | ||
| */ | ||
| export const getAllMyMealsByPeriod = async (startDate: string, endDate: string): Promise<MealDTO[]> => { | ||
| const supabase = getServerClient(); | ||
|
|
||
| const startDateTime = `${startDate}T00:00:00Z`; | ||
| const endDateTime = `${endDate}T23:59:59.999Z`; | ||
|
|
||
| const { data, error } = await supabase | ||
| .from('meals') | ||
| .select(` *, meal_details (*) `) | ||
| .gte('ate_at', startDateTime) | ||
| .lte('ate_at', endDateTime) | ||
| .order('ate_at', { ascending: false }); | ||
| if (error) throw error; | ||
|
|
||
| return snakeToCamelObject<MealSnakeCaseDTO[]>(data); | ||
| }; | ||
|
|
||
| /** | ||
| * 특정 날의 사용자 식사 기록을 모든 상세 정보와 함께 조회한다. | ||
| * | ||
| * @param {string} date - 조회 날짜 ex) 2023-10-15 | ||
| * @returns {MealDTO[]} 사용자의 식사 정보 배열 (MealDTO[]) | ||
| * @throws Supabase 쿼리 실행 중 오류가 발생한 경우 Error | ||
| */ | ||
| export const getMyMealByDate = async (date: string): Promise<MealDTO[]> => { | ||
| const supabase = getServerClient(); | ||
|
|
||
| const { data, error } = await supabase | ||
| .from('meals') | ||
| .select(` *, meal_details (*) `) | ||
| .gte('ate_at', `${date}T00:00:00Z`) | ||
| .lt('ate_at', `${date}T24:00:00Z`) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 부분의 조건문이
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그런가요..? 제가 찾아본 바에 따르면 표준 시간 형식(ISO 8601 포함)에서는 시간을 00:00:00부터 23:59:59까지 표현해서 24:00:00이라는 표기는 다음 날에 해당한다고 해서 여쭤봤습니다! 제가 생각할 때
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아하~ 해당 표기가 비표준이긴 하지만 데이터베이스 쿼리 상에서는 다음 날의 |
||
| .order('ate_at', { ascending: false }); | ||
| if (error) throw error; | ||
|
Comment on lines
+51
to
+57
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기서도 해당유저의 원하는 날짜의 식사가 아닌
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위 질문의 답변과 동일합니다! |
||
|
|
||
| return snakeToCamelObject<MealSnakeCaseDTO[]>(data); | ||
| }; | ||
|
|
||
| /** | ||
| * mealId를 기준으로 사용자 식사 기록을 모든 상세 정보와 함께 조회한다. | ||
| * | ||
| * @param {string} mealId - 조회하려고 하는 meal ID | ||
| * @returns {MealDTO} 사용자의 식사 정보 (MealDTO) | ||
| * @throws Supabase 쿼리 실행 중 오류가 발생한 경우 Error | ||
| */ | ||
| export const getMyMealById = async (mealId: string): Promise<MealDTO> => { | ||
| const supabase = getServerClient(); | ||
|
|
||
| const { data, error } = await supabase | ||
| .from('meals') | ||
| .select(` *, meal_details (*) `) | ||
| .eq('id', mealId) | ||
| .order('ate_at', { ascending: false }) | ||
| .single(); | ||
| if (error) throw error; | ||
|
|
||
| return snakeToCamelObject<MealSnakeCaseDTO>(data); | ||
| }; | ||
|
|
||
| /** | ||
| * 새로운 식사 데이터를 데이터베이스에 생성합니다. | ||
| * | ||
| * @param {CreateMealDTO} meal - 생성할 식사 정보가 담긴 DTO 객체 | ||
| * @returns {Promise<MealOverviewDTO>} 생성된 식사 정보 | ||
| * @throws {Error} 데이터베이스 오류 발생 시 에러를 던집니다 | ||
| */ | ||
| const createMeal = async (meal: CreateMealDTO): Promise<MealOverviewDTO> => { | ||
| const supabase = getServerClient(); | ||
| const mealSnakeCase = camelToSnakeObject(meal); | ||
| const { data, error } = await supabase.from('meals').insert(mealSnakeCase).select().single(); | ||
| if (error) throw error; | ||
| return snakeToCamelObject<MealOverviewSnakeCaseDTO>(data); | ||
| }; | ||
|
|
||
| /** | ||
| * 새로운 상세 식사 데이터를 데이터베이스에 생성합니다. | ||
| * | ||
| * @param {string} mealId - 생성할 식사 정보와 관련된 ID | ||
| * @param {CreateMealDetailDTO[]} mealDetails - 생성할 식사 정보가 담긴 DTO 객체 | ||
| * @returns {Promise<MealDetailDTO[]>} 생성된 상세 식사 정보 | ||
| * @throws {Error} 데이터베이스 오류 발생 시 에러를 던집니다 | ||
| */ | ||
| const createMealDetails = async (mealId: string, mealDetails: CreateMealDetailDTO[]): Promise<MealDetailDTO[]> => { | ||
| const supabase = getServerClient(); | ||
| const mealDetailsRequest = mealDetails.map((mealDetail) => ({ meal_id: mealId, ...camelToSnakeObject(mealDetail) })); | ||
| const { data, error } = await supabase.from('meal_details').insert(mealDetailsRequest).select(); | ||
| if (error) throw error; | ||
| return snakeToCamelObject<MealDetailSnakeCaseDTO[]>(data); | ||
| }; | ||
|
|
||
| /** | ||
| * 새로운 기본 식사 정보와 상세 식사 정보를 데이터베이스에 함께 생성합니다. | ||
| * | ||
| * @param {CreateMealDTO} mealData - 생성할 식사 정보가 담긴 객체 | ||
| * @param {CreateMealDetailDTO[]} mealDetails - 생성할 식사 정보가 담긴 DTO 객체 | ||
| * @returns {Promise<MealDTO>} 생성된 식사 정보 | ||
| * @throws {Error} 데이터베이스 오류 발생 시 에러를 던집니다 | ||
| */ | ||
| export const createMealWithDetails = async ( | ||
| mealData: CreateMealDTO, | ||
| mealDetails: CreateMealDetailDTO[] | ||
| ): Promise<MealDTO> => { | ||
| const meal = await createMeal(mealData); | ||
| const mealDetailResponse = await createMealDetails(meal.id, mealDetails); | ||
| return { ...meal, mealDetails: mealDetailResponse }; | ||
| }; | ||
|
|
||
| /** | ||
| * 특정 식사 데이터를 데이터베이스에 수정합니다. | ||
| * | ||
| * @param {string} mealId - 수정할 식사 정보의 ID | ||
| * @param {Partial<CreateMealDTO>} meal - 수정할 식사 정보가 담긴 DTO 객체 | ||
| * @returns {Promise<MealOverviewDTO|null>} 수정된 식사의 정보 객체 또는 수정 실패 시 null | ||
| * @throws {Error} 데이터베이스 오류 발생 시 에러를 던집니다 | ||
| */ | ||
| export const updateMeal = async (mealId: string, meal: Partial<CreateMealDTO>): Promise<MealOverviewDTO> => { | ||
| const supabase = getServerClient(); | ||
| const mealSnakeCase = camelToSnakeObject(meal); | ||
| const { data, error } = await supabase.from('meals').update(mealSnakeCase).eq('id', mealId).select().single(); | ||
| if (error) throw error; | ||
| return snakeToCamelObject<MealOverviewSnakeCaseDTO>(data); | ||
| }; | ||
|
|
||
| /** | ||
| * 특정 식사 데이터를 데이터베이스에 삭제합니다. | ||
| * | ||
| * @param {string} mealId - 삭제할 식사 정보의 ID | ||
| * @throws {Error} 데이터베이스 오류 발생 시 에러를 던집니다 | ||
| */ | ||
| export const deleteMeal = async (mealId: string) => { | ||
| const supabase = getServerClient(); | ||
| const { error } = await supabase.from('meals').delete().eq('id', mealId); | ||
| if (error) throw error; | ||
| }; | ||
|
|
||
| /** | ||
| * 특정 상세 식사 데이터를 데이터베이스에서 삭제합니다. | ||
| * | ||
| * @param {string} mealDetailId - 삭제할 식사 상세 정보의 ID | ||
| * @throws {Error} 데이터베이스 오류 발생 시 에러를 던집니다 | ||
| */ | ||
| export const deleteMealDetail = async (mealDetailId: string) => { | ||
| const supabase = getServerClient(); | ||
| const { error } = await supabase.from('meal_details').delete().eq('id', mealDetailId); | ||
| if (error) throw error; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| 'use server'; | ||
|
|
||
| import { getServerClient } from '@/lib/utils/supabase/server.util'; | ||
| import { sanitizeFilename } from '@/lib/utils/filename.util'; | ||
| import { categoriesError, ErrorResponse } from '@/types/error.type'; | ||
| import { SupabaseBucketValue } from '@/types/supabase-bucket.type'; | ||
|
|
||
| /** | ||
| * Supabase 스토리지의 지정된 버킷에 이미지 파일을 업로드합니다. | ||
| * | ||
| * 이 함수는 파일명을 고유한 UUID와 안전하게 처리된 원본 파일명의 조합으로 생성하고, | ||
| * 파일을 업로드한 후 공개 URL을 반환합니다. | ||
| * | ||
| * @async | ||
| * @param {SupabaseBucketValue} bucketName - 업로드할 Supabase 버킷 이름 ('meal' 또는 'profile-image') | ||
| * @param {FormData} formData - 업로드할 파일 객체 | ||
| * @returns {Promise<ErrorResponse<string>>} 성공 시 data에 업로드된 파일의 공개 URL을 포함하고, 실패 시 error에 오류 정보를 포함하는 객체 | ||
| */ | ||
| export const uploadImage = async ( | ||
| bucketName: SupabaseBucketValue, | ||
| formData: FormData | ||
| ): Promise<ErrorResponse<string>> => { | ||
| const supabase = getServerClient(); | ||
|
|
||
| const file = formData.get('file') as File; | ||
| const filename = crypto.randomUUID() + sanitizeFilename(file.name); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 파일이름을 정제하는 것을 sanitize라고 하는군요! |
||
|
|
||
| const { error: uploadError } = await supabase.storage.from(bucketName).upload(filename, file); | ||
| if (uploadError) return { data: null, error: categoriesError(uploadError) }; | ||
|
|
||
| const { data: fileUrl } = supabase.storage.from(bucketName).getPublicUrl(filename); | ||
| return { data: fileUrl.publicUrl, error: null }; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| 'use server'; | ||
|
|
||
| import { getAuth } from '@/lib/apis/auth-server.api'; | ||
| import { camelToSnakeObject, snakeToCamelObject } from '@/lib/utils/camelize.util'; | ||
| import { getServerClient } from '@/lib/utils/supabase/server.util'; | ||
| import { UpdateUserDTO, UserDTO } from '@/types/DTO/user.dto'; | ||
|
|
||
| /** | ||
| * 자신의 정보를 불러오는 함수 | ||
| * | ||
| * @returns {Promise<UserDTO>} 자신의 정보가 담긴 객체 | ||
| */ | ||
| export const getUser = async (): Promise<UserDTO> => { | ||
| const supabase = getServerClient(); | ||
| const { data, error } = await supabase.from('users').select().single(); | ||
| if (error) throw error; | ||
| return snakeToCamelObject(data); | ||
| }; | ||
|
|
||
| /** | ||
| * 유저 정보를 변경하는 함수 | ||
| * | ||
| * @param {Partial<UpdateUserDTO>} userInfo 수정할 일부 정보 | ||
| * @returns {Promise<UserDTO>} 변경 이수 수정된 값 | ||
|
||
| */ | ||
| export const updateUser = async (userInfo: Partial<UpdateUserDTO>): Promise<UserDTO> => { | ||
| const supabase = getServerClient(); | ||
| const { id: userId } = await getAuth(); | ||
| const userInfoSnakeCase = camelToSnakeObject(userInfo); | ||
| const { data, error } = await supabase.from('users').update(userInfoSnakeCase).eq('id', userId).select().single(); | ||
| if (error) throw error; | ||
| return snakeToCamelObject(data); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| /** | ||
| * 파일명이 S3 객체 키로 안전한 문자만 포함하는지 검증합니다. | ||
| * | ||
| * 이 함수는 파일명이 Amazon S3 객체 키에서 특별한 처리 없이 사용할 수 있는 | ||
| * 안전한 문자와 특별한 처리가 필요하지만 여전히 유효한 일부 문자만 포함하는지 확인합니다. | ||
| * | ||
| * 안전한 문자는 다음과 같습니다: | ||
| * - 영숫자 (a-z, A-Z, 0-9) | ||
| * - 밑줄 (_) | ||
| * - 슬래시 (/) | ||
| * - 느낌표 (!) | ||
| * - 하이픈 (-) | ||
| * - 마침표 (.) | ||
| * - 별표 (*) | ||
| * - 작은따옴표 (') | ||
| * - 괄호 ( () ) | ||
| * - 공백 ( ) | ||
| * - 앰퍼샌드 (&) | ||
| * - 달러 기호 ($) | ||
| * - 골뱅이 (@) | ||
| * - 등호 (=) | ||
| * - 세미콜론 (;) | ||
| * - 콜론 (:) | ||
| * - 더하기 기호 (+) | ||
| * - 쉼표 (,) | ||
| * - 물음표 (?) | ||
| * | ||
| * @see {@link https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html AWS S3 객체 키 문서} | ||
| * | ||
| * @param {string} filename - 검증할 파일명 | ||
| * @returns {boolean} - 파일명이 S3 안전 문자만 포함하면 true, 그렇지 않으면 false 반환 | ||
| */ | ||
| export const isValidFilename = (filename: string): boolean => { | ||
| // only allow s3 safe characters and characters which require special handling for now | ||
| // https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html | ||
| return /^(\w|\/|!|-|\.|\*|'|\(|\)| |&|\$|@|=|;|:|\+|,|\?)*$/.test(filename); | ||
| }; | ||
|
|
||
| /** | ||
| * 파일명을 S3 객체 키에서 사용 가능한 안전한 형식으로 변환합니다. | ||
| * | ||
| * 이 함수는 S3에서 허용되지 않는 문자를 제거하거나 안전한 문자로 대체합니다. | ||
| * 특별한 처리가 필요한 문자들도 적절히 처리합니다. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 특별한 처리가 필요한 문자란게 &$@=;:+,?를 말씀하신 것 같습니다.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 제가 코드로 참고 링크를 남긴다고 메모도 해두었는데 까먹었습니다! 참고 링크 :
|
||
| * | ||
| * @param {string} filename - 변환할 원본 파일명 | ||
| * @param {string} [replacement='_'] - 안전하지 않은 문자를 대체할 문자 | ||
| * @returns {string} S3에서 사용 가능한 안전한 파일명 | ||
| */ | ||
| export function sanitizeFilename(filename: string, replacement: string = '_'): string { | ||
| if (!filename || typeof filename !== 'string') { | ||
| return ''; | ||
| } | ||
| const safeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_/!-.*'() &$@=;:+,?"; | ||
|
|
||
| let sanitized = ''; | ||
| for (const char of filename) { | ||
| sanitized += safeChars.includes(char) ? char : replacement; | ||
| } | ||
|
|
||
| // 시작과 끝의 공백 제거 | ||
| sanitized = sanitized.trim(); | ||
|
|
||
| // 연속된 대체 문자를 하나로 압축 (예: '___' -> '_') | ||
| const regexPattern = new RegExp(`\\${replacement}{2,}`, 'g'); | ||
| sanitized = sanitized.replace(regexPattern, replacement); | ||
|
|
||
| // 파일명 시작/끝에 있는 대체 문자 제거 | ||
| const startEndPattern = new RegExp(`^\\${replacement}|\\${replacement}$`, 'g'); | ||
| sanitized = sanitized.replace(startEndPattern, ''); | ||
|
|
||
| // 빈 문자열이 되면 기본값 설정 | ||
| if (sanitized === '') sanitized = 'file'; | ||
|
|
||
| return sanitized; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로그인한 유저 기준이 아니라 전체 식사데이터를 불러오고 있는것 같습니다
저희가 전체 기간에 모든 유저의 데이터를 받아오는 경우가 있을까요?
아니라면 .eq(user_id, userId) 조건이 필요할 것 같습니다!
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ㅋㅋㅋㅋ 이 질문 기다렸습니다!
저렇게만 되어 있어도 자신이 작성한 내용만 가져오게끔됩니다.

왜냐! supabase의 RLS SELECT 설정에서 자신이 작성한 글만 조회가능하도록 제한을 걸어놨거든요!
그래서 자신이 작성한 글 외에는 조회 자체가 안되게 되어서 client에서 한 번 더 조건을 작성할 필요가 없게된겁니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아니 이런 방법이...? 미쳤네요! supabase에서 저런 설정이 가능했군요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 그럼 유저 프로필도 굳이 id 대조안하고 자기 정보만 가져오게 할 수 있겠당!