[간단식단] 식단생성 구현 V0.1

어후러·2023년 6월 14일

메인기능인 식단생성을 구현하면서 마주한 문제와 해결했던 경험을 적어보고자 합니다.

  1. 냉장고에 다 들어가는가?
  2. 식단 중복을 방지했는가?
  3. 정말 다이어트가 되도록 만들었는가?
  4. 확장과 수정이 쉬운가?

위의 4가지를 중점적으로 기획/구현 했습니다.

하나하나 어떻게 구현했지 작성해 보겠습니다.

1. 냉장고에 다 들어가는가?

이 문제는 한가지 식단을 주에 21번 반복하면서 생긴 문제였고 내가 생각한 해결방법은 다음과 같다.

  1. 대부분 쿠팡에 있는 음식을 사용해 대용량이니 마켙컬리나 다른 사이트에 있는 식품을 추가한다.
  2. 카테고리를 지정해서 3일치 식단을 2번 반복해 6일치 식단을 제공하자.
    - 이렇게 하면 질리지 않고 오래 사이트를 유지할 수 있다고 생각했다.
    그외에도 냉장고 사이즈를 입력받기, 실온 식품과 냉장식품을 나누어서 비율을 맞춘다

등이 있었지만 2번방법을 택했다.

왜냐하면 결제자체를 쉽게만들 수 있는 방법이 1번이 가장 적합했기 때문이다.

그렇게 3가지의 카테고리를 선택해서 2번 추가하는 로직으로 만들었다.

2. 식단 중복 방지

하나의 diet에는 N개의 식품이있기 때문에 여러개의 식단이 만들어지는 특성상 manytomanyfield로 구현 되었다.

그렇게 테스트를 돌려보니중복되는 식단을 계속해서 만들어지는 버그가 있었다.
내 검색의 한계로는 결국 식단의 ID가 다르기 때문에 같은 음식이 들어와 있어도 다르다라고 인식한다는 것을 알았다

결국에는 식단을 만든 이후 그 식단과 길이와 음식의 리스트가 똑같은 것이 있다면 그 식단을 리턴하는 로직을 해결했다.

    def make_instance(self, min_nutrient, meal_count, category, bulk=False):
        diet_data = init_nutrient()
        meal_list = []
        
        for _, nutrient_range in zip(self.__meals, self.__meals_nutrient):
            need_nutrient = cal_nutrient_range(min_nutrient, nutrient_range)
            
            meal_getter = MealGetter(Meal)
            _meal = meal_getter.get_data(need_nutrient, category)
            add_nutrient(diet_data, _meal)
            meal_list.append(_meal)

        if bulk:
            return diet_data
                    
        queryset = self.model.objects.filter(meals__in=meal_list, meal_count=meal_count, category=category)
        if queryset.exists():
            for diet in queryset:
                if len(diet.meals.all()) == len(meal_list):
                    return diet
                
        diet = self.model.objects.create(
            kcal=diet_data["kcal"],
            protein=diet_data["protein"],
            fat=diet_data["fat"],
            carbs=diet_data["carbs"],
            meal_count = meal_count,
            category = category
        )
        diet.meals.set(meal_list)
        diet.save()
        return diet

코드자체는 구질구질하지만 다음에는 멘토링을 받아볼만한 코드라고 생각한다.

3. 정말 다이어트가 되도록 만들었는가?

https://www.youtube.com/watch?v=dJoOsjMBA9U&list=PLeRLqDfWwd9P-2Y5n5FYH1RxBte0klGjB

https://www.youtube.com/watch?v=yTWdaj7iMYo&list=PLeRLqDfWwd9P-2Y5n5FYH1RxBte0klGjB

위의 2가지를 토대로 구현했지만 프론트를 처음하다 보니 이거 맞나 싶을정도록 사용성이 너무 후지다.
ver0.3 부터는 프로젝트를 같이 만들어 볼생각이다. (아래는 0.1.2)

4. 확장과 수정이 쉬운가?

이번 프로젝트를 진행하면서 어러번의 수정을 거듭했다.
스파게티가 너무 많아져 대대적으로 코드를 작성하는 방법을 배웠다.

다음은 최적화를 한부분 들이다.

APP이 아닌 코드는 분리하기


영양소계산이나 기초대사량을 구하는 것은 유틸로 빼자니 크기가 있어서 core쪽으로 분리 했다.

클래스를 만들어 중복 최소화

전체적인 식단을 생성하는 로직은 다음과 같다.
1. 옵션에 해당하는 주간 식단이 있는가?(옵션 : 끼니, 식품카테고리들)
2. 없다면 주간식단을 생성한다.
3. 옵션에 해당하는 식단이 있는가? (옵션 : 끼니, 식품카테고리)
4. 없다면 식단을 생성한다.
5. 옵션에 해당하는 식사가 있는가? (옵션 : 식품카테고리)
6. 없다면 식사를 생성한다.
7. 식품 카테고리 옵션에 해당하는 식품을 정렬하고 유저의 기초대사량에 맞도록 식품데이터를 돌린다.


주기적으로 반복되는 있는가? 만든다! 를 getter와 maker로 분리하여서 진행했다.(글을 작성하고 있는데 인터페이스가 없다고 생각된다.)

Getter

다음은 기본적인 getter이다

# base_getter.py

class GetterBase(ManagerBase):
    def __init__(self, _model):
        super().__init__(_model)

    def find_instance(self, model, min_nutrient, max_nutrient, meal_count=None, category=None):
        q = Q()

        for nutrient in ["kcal", "protein", "fat", "carbs"]:
            nutrient_min = "{}".format(nutrient)
            nutrient_max = "{}".format(nutrient)
            q &= Q(**{"{}__gte".format(nutrient_min): min_nutrient[nutrient], "{}__lte".format(nutrient_max): max_nutrient[nutrient]})
            if category is not None:
                q &= Q(category=category)
            if meal_count is not None:
                q &= Q(meal_count=meal_count)
        return model.objects.filter(q)
    
    @abstractmethod
    def get_data(self):
        pass

get_data : 말그대로 데이터를 가져오는거다 네이밍이 아쉽지만...
find_instance : 영양소를 비교하여 가저오는 로직이다.

아래는 WeekDietGetter이다.

#weekdiet_getter.py
class WeekDietGetter(GetterBase):
    def __init__(self):
        pass
    
    def find_instance(self, model:WeekDiet, min_nutrient, max_nutrient, meal_count=None, categories=None):
        q = Q()
        for nutrient in ["kcal", "protein", "fat", "carbs"]:
            nutrient_min = "{}".format(nutrient)
            nutrient_max = "{}".format(nutrient)
            q &= Q(**{"{}__gte".format(nutrient_min): min_nutrient[nutrient], "{}__lte".format(nutrient_max): max_nutrient[nutrient]})
            q &= Q(categories__in=categories)
            if meal_count is not None:
                q &= Q(meal_count=meal_count)
        return model.objects.filter(q)


    def get_data(self, meal_count, min_nutrient, max_nutrient, categories):
        week_min_nutrient = cal_nutrient_range(min_nutrient, 6)
        week_max_nutrient = cal_nutrient_range(max_nutrient, 6)
        
        week_diet = self.find_instance(WeekDiet, week_min_nutrient, week_max_nutrient, meal_count, categories)

        # TODO : 0번째인지는 미정이다.
        if week_diet.count() > 0:
            return week_diet[0] 
        else :
            week_maker = WeekDietMaker(WeekDiet)
            return week_maker.make_instance(meal_count, min_nutrient, max_nutrient, categories)

왜 find_instance를 재정의 했냐면 weekdiet만 카테고리가 manytomany filed로 지정되어 있어서 그렇다
diet나 meal 같은 경우에는 아래와 같이 구현했다.

class DietGetter(GetterBase):
    def __init__(self, model, meal_count):
        super().__init__(model)
        self.maker = DietMaker(model, meal_count)

    def get_data(self, min_nutrient, max_nutrient, meal_count, category):
        diet = self.find_instance(Diet, min_nutrient, max_nutrient, meal_count, category)
        if diet.count() > 0:
            return diet.first()
        else :
            return self.maker.make_instance(min_nutrient, meal_count, category)

maker

다음은 기본적인 maker이다.

class MakerBase(ManagerBase):
    def __init__(self, _model):
        super().__init__(_model)

    @abstractmethod
    def make_instance(self):
        pass



위를 상속해서 만든 weekdiet_maker이다.

class WeekDietMaker(MakerBase):
    def __init__(self, model):
        super().__init__(model)

    def make_instance(self, meal_count, min_nutrient, max_nutrient, categories):
        week_nutrient_data = init_nutrient()
        diet_list = []
        
        for category in categories:
            diet_getter = DietGetter(Diet, meal_count)
            _diet = diet_getter.get_data(min_nutrient, max_nutrient, meal_count, category)
            # 2번 더해주는 것이 옳다.
            add_nutrient(week_nutrient_data, _diet)
            add_nutrient(week_nutrient_data, _diet)
            diet_list.append(_diet)
        diet_list = diet_list * 2
        
        week_diet = self.model.objects.create(
            kcal=week_nutrient_data["kcal"],
            protein=week_nutrient_data["protein"],
            fat=week_nutrient_data["fat"],
            carbs=week_nutrient_data["carbs"],
            meal_count = meal_count,
        )
        week_diet.diets.set(diet_list)

        return week_diet

후기

v0.1 버전 회고를 마무리하고 다음 포스트때 돌아오겠습니다. (취업하고 싶다.)

profile
작게 하나씩

0개의 댓글