💡 AI 인사이트

🤖 AI가 여기에 결과를 출력합니다...

댓글 커뮤니티

쿠팡이벤트

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

검색

    로딩 중이에요... 🐣

    [코담] 웹개발·실전 프로젝트·AI까지, 파이썬·장고의 모든것을 담아낸 강의와 개발 노트

    3 1. model 작성 | ✅ 저자: 이유정(박사)

    https://docs.google.com/spreadsheets/d/1doTGceNAjBx3BMTKK5jJ8jyu2Caw3iX-E5QMUupykPs/edit?usp=sharing

    • 1:N (O2M): ForeignKey
    • M:N (M2M): ManyToManyField
    • 1:1 (O2O): OneToOneField

    Article 테이블
    필드 이름 데이터 타입 한글 필드명 옵션
    id AutoField (PK)
    title CharField<br>(max_length=100) 제목 db_index=True
    preview_image ImageField<br>(upload_to="article") 미리보기 이미지 null=True, blank=True
    content TextField 내용
    show_at_index BooleanField<br>(default=False) 첫 페이지 노출 여부
    is_published BooleanField<br>(default=False) 발행 여부
    created_at DateTimeField<br>(auto_now_add) 생성일
    modified_at DateTimeField<br>(auto_now=True) 수정일
    관계: 없음
    • 역할: 사이트 첫 페이지나 블로그 섹션에 노출될 콘텐츠(기사·칼럼)를 관리
    • 관리 기능:
      • 제목·본문 작성 및 수정
      • 발행 여부(is_published)·첫 페이지 노출 여부(show_at_index) 토글
      • 미리보기 이미지 업로드
      • 게시일(created_at) 및 최종 수정일(modified_at) 확인
    class Article(models.Model):
        title = models.CharField(max_length=100, db_index=True)
        preview_image = models.ImageField(upload_to="article", null=True, blank=True)
        content = models.TextField()
        show_at_index = models.BooleanField(default=False)
        is_published = models.BooleanField(default=False)
        created_at = models.DateTimeField("생성일", auto_now_add=True)
        modified_at = models.DateTimeField(auto_now=True)
    

    title = models.CharField(...)

    • 문자열 필드, 제목을 저장하는 용도
    • max_length=100 → 최대 100자까지 저장 가능
    • db_index=True → 해당 필드에 DB 인덱스 생성 → 검색 속도 향상

    preview_image = models.ImageField(...)

    • 이미지 업로드용 필드
    • upload_to="article"media/article/ 폴더에 이미지 저장됨
    • null=True → DB에서 이 값이 없어도 허용됨 (NULL 저장 가능)
    • blank=True → 폼(form)에서 입력을 생략할 수 있음

    content = models.TextField()

    • 글 본문을 저장할 필드
    • 긴 텍스트를 저장할 때 사용
    • TextField()CharField와 달리 max_length 제한 없음

    show_at_index = models.BooleanField(default=False)

    • True/False 값을 저장하는 필드
    • 메인 페이지에 노출 여부를 판단하는 용도
    • default=False → 기본값은 “표시 안 함”

    is_published = models.BooleanField(default=False)

    • 게시 여부를 나타냄
    • default=False → 기본적으로 미공개 상태

    created_at = models.DateTimeField(auto_now_add=True)

    • 객체(Article)가 처음 생성될 때의 시간을 자동 저장
    • auto_now_add=True 덕분에 개발자가 따로 시간을 넣지 않아도 자동 저장됨

    modified_at = models.DateTimeField(auto_now=True)

    • 객체가 수정될 때마다 현재 시간으로 자동 갱신**됨
    • 예: 글 수정할 때마다 이 필드가 최신 시간으로 바뀜
    class Meta:
    	verbose_name = "칼럼"
    	verbose_name_plural = "칼럼"
    
    • 모델의 메타데이터(설정)를 정의하는 클래스입니다.
    • verbose_name: admin 화면에서 이 모델을 "칼럼" 이라는 이름으로 표시
    • verbose_name_plural: 복수형도 "칼럼" 으로 표시 (보통은 자동으로 "칼럼s"가 되는데 그걸 방지)
    def __str__(self):
    	return f"{self.id} - {self.title}"
    
    • 객체를 문자열로 표현할 때 사용됩니다.
    • 관리자(admin), 쉘, 디버깅 시 다음처럼 표시됨
    • 이걸 넣지 않으면 객체 출력 시 <Article: Article object (5)>처럼 나오기 때문에 사람이 보기 불편합니다.

    Restaurant 테이블
    필드 이름 데이터 타입 한글 필드명 옵션
    id AutoField (PK)
    name CharField<br>(max_length=100) 이름 db_index=True
    branch_name CharField<br>(max_length=100) 지점 null=True, blank=True, db_index=True
    description TextField 설명 null=True, blank=True
    address CharField<br>(max_length=255) 주소 db_index=True
    feature CharField<br>(max_length=255) 특징
    is_closed BooleanField<br>(default=False) 폐업 여부
    latitude DecimalField<br>(max_digits=16, decimal_places=12) 위도 db_index=True, default="0.0000"
    longitude DecimalField<br>(max_digits=16, decimal_places=12) 경도 db_index=True, default="0.0000"
    phone CharField<br>(max_length=16) 전화번호 help_text="E.164 포맷", null=True, blank=True
    rating DecimalField<br>(max_digits=3, decimal_places=2) 평점 default="0.0"
    rating_count PositiveIntegerField 평가수 default=0
    start_time TimeField 영업 시작 시간 null=True, blank=True
    end_time TimeField 영업 종료 시간 null=True, blank=True
    last_order_time TimeField 라스트 오더 시간 null=True, blank=True
    category ForeignKey → RestaurantCategory 가게 카테고리 (1:N, on_delete=SET_NULL, null=True, blank=True)
    tags ManyToManyField → Tag 태그 (M:N, blank=True)
    region ForeignKey →<br>Region 지역 on_delete=SET_NULL, null=True, blank=True, related_name="restaurants"

    관계

    • RestaurantCategory ← 1:N
    • Tag ← M:N
    • 뒤에 나오는 RestaurantImage, RestaurantMenu, Review 모델이 각각 Restaurant에 대해 1:N 관계를 맺음

    역할:

    • 실제 식당(매장) 하나하나의 기본 정보(이름·지점·주소·영업시간·위치·전화번호 등)를 관리 관리 기능:
    • 신규 레스토랑 등록 및 기존 정보 수정
    • 영업 상태(영업 중/폐업) 토글
    • 위치 정보(위도·경도) 설정 → 지도 표시
    • 분류(Category)·태그(Tag) 지정 → 검색·필터링
    class Restaurant(models.Model):
        name = models.CharField("이름", max_length=100, db_index=True)
        branch_name = models.CharField(
            "지점", max_length=100, db_index=True, null=True, blank=True
        )
        description = models.TextField("설명", null=True, blank=True)
        address = models.CharField("주소", max_length=255, db_index=True)
        feature = models.CharField("특징", max_length=255)
        is_closed = models.BooleanField("폐업 여부", default=False)
        latitude = models.DecimalField(
            "위도",
            max_digits=16,
            decimal_places=12,
            db_index=True,
            default="0.0000",
        )
        longitude = models.DecimalField(
            "경도",
            max_digits=16,
            decimal_places=12,
            db_index=True,
            default="0.0000",
        )
        phone = models.CharField(
            "전화번호", max_length=16, help_text="E.164 포맷", blank=True, null=True
        )
        rating = models.DecimalField("평점", max_digits=3, decimal_places=2, default="0.0")
        rating_count = models.PositiveIntegerField("평가수", default=0)
        start_time = models.TimeField("영업 시작 시간", null=True, blank=True)
        end_time = models.TimeField("영업 종료 시간", null=True, blank=True)
        last_order_time = models.TimeField("라스트 오더 시간", null=True, blank=True)
        category = models.ForeignKey(
            "RestaurantCategory", on_delete=models.SET_NULL, blank=True, null=True
        )
        tags = models.ManyToManyField("Tag", blank=True)
        region = models.ForeignKey(
            "Region",
            on_delete=models.SET_NULL,
            null=True,
            blank=True,
            verbose_name="지역",
            related_name="restaurants",
        )
    
        class Meta:
            verbose_name = "레스토랑"
            verbose_name_plural = "레스토랑"
    
        def __str__(self):
            return f"{self.name} {self.branch_name}" if self.branch_name else f"{self.name}"
    
    Restaurant 모델 필드 속성 설명표
    속성명 설명
    max_length 문자열(CharField) 입력의 최대 글자 수를 제한합니다. (name, branch_name, feature, phone 등)
    db_index 해당 필드에 DB 인덱스를 생성하여 검색 속도를 향상시킵니다. (name, branch_name, address, latitude, longitude)
    null DB에 NULL 값을 저장할 수 있게 허용합니다. (branch_name, description, phone, start_time 등)
    blank 폼(입력 폼 등)에서 비워도 유효하게 허용합니다. (branch_name, description, tags, region 등)
    default 값이 입력되지 않았을 때 기본값을 지정합니다. (latitude, longitude, rating, rating_count, is_closed)
    help_text 관리자 페이지에서 입력 시 도움말로 표시됩니다. (phone에 "E.164 포맷" 설명 포함)
    max_digits DecimalField에서 전체 자릿수(정수부 + 소수부 포함)를 지정합니다. (latitude, longitude, rating)
    decimal_places 소수점 아래 자릿수를 지정합니다. (latitude, longitude, rating)
    verbose_name 관리자 페이지 등에서 보여질 필드의 한글 또는 커스텀 이름입니다. ("이름", "주소", "전화번호" 등)
    on_delete ForeignKey가 참조하는 객체 삭제 시 동작 정의입니다. (models.SET_NULL로 설정됨)
    related_name 역참조 시 사용할 이름입니다. 예: region.restaurants.all()
    TimeField 시간을 저장하는 필드 타입입니다. (start_time, end_time, last_order_time)
    TextField 길이 제한 없는 문자열 필드입니다. (description)
    CharField 길이 제한이 있는 문자열 필드입니다.
    DecimalField 소수점을 포함한 숫자 값을 저장합니다.
    PositiveIntegerField 양의 정수만 저장하는 필드입니다. (rating_count)
    BooleanField 참/거짓 값을 저장하는 필드입니다. (is_closed)
    ForeignKey 다른 모델과의 다대일 관계를 설정합니다. (category, region)
    ManyToManyField 다른 모델과의 다대다 관계를 설정합니다. (tags)

    CuisineType (음식 종류)
    필드 이름 데이터 타입 한글 필드명 옵션
    id AutoField (PK)
    name CharField(max_length=20) 이름
    관계
    • RestaurantCategory ← 1:N

    역할:

    • CuisineType: 한식·양식·일식 등 상위 음식 분류
    • RestaurantCategory: 스테이크·파스타·비빔밥 등 보다 세부적인 업장 분류 관리 기능:
    • 분류 계층 구조 관리 → 레스토랑 등록 시 선택지 제공
    • 필요 시 분류 삭제·추가
    class CuisineType(models.Model):
        name = models.CharField("이름", max_length=20)
    
        class Meta:
            verbose_name = "음식 종류"
            verbose_name_plural = "음식 종류"
    
        def __str__(self):
            return self.name
    

    RestaurantCategory (가게 카테고리)
    필드 이름 데이터 타입 한글 필드명 옵션
    id AutoField (PK)
    name CharField<br>(max_length=20) 이름
    cuisine_type ForeignKey → CuisineType 음식 종류 (1:N, on_delete=CASCADE, null=True, blank=True)
    관계
    • CuisineType ← 1:N

    역할:

    • CuisineType: 한식·양식·일식 등 상위 음식 분류
    • RestaurantCategory: 스테이크·파스타·비빔밥 등 보다 세부적인 업장 분류 관리 기능:
    • 분류 계층 구조 관리 → 레스토랑 등록 시 선택지 제공
    • 필요 시 분류 삭제·추가
    class RestaurantCategory(models.Model):
        name = models.CharField("이름", max_length=20)
        cuisine_type = models.ForeignKey(
            "CuisineType",
            on_delete=models.CASCADE,
            null=True,
            blank=True,
        )
    
        class Meta:
            verbose_name = "가게 카테고리"
            verbose_name_plural = "가게 카테고리"
    
        def __str__(self):
            return self.name
    

    RestaurantImage (가게 이미지)
    필드 이름 데이터 타입 한글 필드명 옵션
    id AutoField (PK)
    restaurant ForeignKey → Restaurant 레스토랑 (참조) (1:N, on_delete=CASCADE)
    is_representative BooleanField<br>(default=False) 대표 이미지 여부
    order PositiveIntegerField 순서 null=True, blank=True
    name CharField<br>(max_length=100) 이름 null=True, blank=True
    image ImageField|(upload_to="restaurant") 이미지
    created_at DateTimeField<br>(auto_now_add=True) 생성일 db_index=True
    updated_at DateTimeField<br>(auto_now=True) 수정일 db_index=True

    관계

    • Restaurant ← 1:N

    역할: 레스토랑별 이미지(전경 사진·대표 메뉴 사진 등) 관리 관리 기능:

    • 여러 장 업로드 가능
    • 대표 이미지(is_representative) 지정 → 목록·상세 페이지 썸네일
    • 순서(order) 지정 → 슬라이더 노출 순서
    class RestaurantImage(models.Model):
        restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
        is_representative = models.BooleanField("대표 이미지 여부", default=False)
        order = models.PositiveIntegerField("순서", null=True, blank=True)
        name = models.CharField("이름", max_length=100, null=True, blank=True)
        image = models.ImageField("이미지", max_length=100, upload_to="restaurant")
        created_at = models.DateTimeField("생성일", auto_now_add=True, db_index=True)
        updated_at = models.DateTimeField("수정일", auto_now=True, db_index=True)
    
        class Meta:
            verbose_name = "가게 이미지"
            verbose_name_plural = "가게 이미지"
    
        def __str__(self):
            return f"{self.id}:{self.image}"
    
        def clean(self):
            images = self.restaurant.restaurantimage_set.filter(is_representative=True)
            if self.is_representative and images.exclude(id=self.id).count() > 0:
                raise ValidationError("대표 이미지는 1개만 지정 가능합니다.")
    
    속성명 설명
    default=False BooleanField의 기본값 설정 (is_representative 필드)
    upload_to="restaurant" 업로드 파일의 저장 경로를 지정 (image 필드에서 사용)
    auto_now_add=True 최초 생성 시 현재 시간 자동 저장 (created_at 필드)
    auto_now=True 저장 시마다 현재 시간 자동 업데이트 (updated_at 필드)
    settings.py에서 이렇게 되어 있다면:
    MEDIA_ROOT = BASE_DIR / 'media'
    MEDIA_URL = '/media/'
    
    업로드된 파일은 실제로:
    your_project/
    └── media/
        └── restaurant/
            └── burger.png
    

    장고가 자동으로 생성해 줍니다.


    RestaurantMenu (가게 메뉴)
    필드 이름 데이터 타입 한글 필드명 옵션
    id AutoField (PK)
    restaurant ForeignKey → Restaurant 레스토랑 (참조) (1:N, on_delete=CASCADE)
    name CharField(max_length=100) 이름
    price PositiveIntegerField 가격 default=0
    image ImageField<br>(upload_to="restaurant-menu") 이미지 null=True, blank=True
    created_at DateTimeField<br>(auto_now_add=True) 생성일 db_index=True
    updated_at DateTimeField<br>(auto_now=True) 수정일 db_index=True

    관계

    • Restaurant ← 1:N

    역할: 레스토랑별 판매 메뉴(메뉴명·가격·메뉴 사진) 관리 관리 기능:

    • 메뉴 아이템 추가·삭제·수정
    • 가격 정보 일괄 업데이트
    • 메뉴 사진 첨부 → 고객에게 메뉴 이미지 제공
    class RestaurantMenu(models.Model):
        restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
        name = models.CharField("이름", max_length=100)
        price = models.PositiveIntegerField("가격", default=0)
        image = models.ImageField(
            "이미지", upload_to="restaurant-menu", null=True, blank=True
        )
        created_at = models.DateTimeField("생성일", auto_now_add=True, db_index=True)
        updated_at = models.DateTimeField("수정일", auto_now=True, db_index=True)
    
        class Meta:
            verbose_name = "가게 메뉴"
            verbose_name_plural = "가게 메뉴"
    
        def __str__(self):
            return self.name
    

    Review (리뷰)
    필드 이름 데이터 타입 한글 필드명 옵션
    id AutoField (PK)
    title CharField<br>(max_length=100) 제목
    author CharField<br>(max_length=100) 작성자
    profile_image ImageField<br>(upload_to="review-profile") 프로필 이미지 blank=True, null=True
    content TextField 내용
    rating PositiveSmallIntegerField<br>(validators=[MinValueValidator(1), MaxValueValidator(5)]) 평점
    restaurant ForeignKey → Restaurant 레스토랑 (참조) (1:N, on_delete=CASCADE)
    social_channel ForeignKey → SocialChannel 소셜채널 (참조) (1:N, on_delete=SET_NULL, null=True,blank=True)
    created_at DateTimeField<br>(auto_now_add=True) 생성일 db_index=True
    updated_at DateTimeField<br>(auto_now=True) 수정일 db_index=True

    관계 - Restaurant ← 1:N - SocialChannel ← 1:N

    역할: 고객·외부 채널(블로그·SNS)에서 작성된 리뷰와 그에 딸린 이미지 관리 관리 기능:

    • 리뷰 제목·내용·평점·작성자 정보 확인
    • 리뷰별 이미지 첨부 관리
    • 소셜 채널 연동 확인(social_channel) → 리뷰 출처 표시
    • 최신순 정렬(ordering = ["-created_at"])
    class Review(models.Model):
        title = models.CharField("제목", max_length=100)
        author = models.CharField("작성자", max_length=100)
        profile_image = models.ImageField(
            "프로필 이미지", upload_to="review-profile", blank=True, null=True
        )
        content = models.TextField("내용")
        rating = models.PositiveSmallIntegerField(
            validators=[MinValueValidator(1), MaxValueValidator(5)]
        )
        restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
        social_channel = models.ForeignKey(
            "SocialChannel", on_delete=models.SET_NULL, blank=True, null=True
        )
        created_at = models.DateTimeField("생성일", auto_now_add=True, db_index=True)
        updated_at = models.DateTimeField("수정일", auto_now=True, db_index=True)
    
        class Meta:
            verbose_name = "리뷰"
            verbose_name_plural = "리뷰"
            ordering = ["-created_at"]
    
        def __str__(self):
            return f"{self.author}:{self.title}"
    
        @property
        def restaurant_name(self):
            return self.restaurant.name
    
        @property
        def content_partial(self):
            return self.content[:20]
    
    요소 종류 설명
    upload_to="review-profile" 필드 속성 업로드된 이미지를 media/review-profile/ 폴더에 저장 (폴더는 자동 생성됨)
    PositiveSmallIntegerField 필드 타입 0보다 큰 작은 정수만 저장 가능 (1~32767) – rating에 적절
    validators=[MinValueValidator, MaxValueValidator] 필드 속성 숫자 범위 제한 (여기선 1 이상, 5 이하로 제한)
    on_delete=models.SET_NULL 필드 속성 참조된 객체 삭제 시, 이 필드는 NULL로 설정됨 (social_channel)
    auto_now_add=True 필드 속성 생성 시 현재 시간 자동 저장 (created_at)
    auto_now=True 필드 속성 저장 시마다 현재 시간 자동 업데이트 (updated_at)
    db_index=True 필드 속성 해당 필드에 인덱스를 생성하여 검색 속도 향상 (created_at, updated_at)
    ordering = ["-created_at"] 메타 옵션 기본 정렬 기준 설정 (created_at 기준 내림차순)
    @property 파이썬 데코레이터 메서드를 속성처럼 사용할 수 있게 만듬 (예: review.restaurant_name)

    ReviewImage (리뷰이미지)
    필드 이름 데이터 타입 한글 필드명 옵션
    id AutoField (PK)
    review ForeignKey → Review 리뷰 (참조) (1:N, on_delete=CASCADE)
    name CharField<br>(max_length=100) 이름
    image ImageField<br>(upload_to="review") 이미지
    created_at DateTimeField<br>(auto_now_add=True) 생성일 db_index=True
    updated_at DateTimeField<br>(auto_now=True) 수정일 db_index=True

    관계

    • Review ← 1:N
    class ReviewImage(models.Model):
        review = models.ForeignKey(Review, on_delete=models.CASCADE)
        name = models.CharField(max_length=100)
        image = models.ImageField(max_length=100, upload_to="review")
        created_at = models.DateTimeField("생성일", auto_now_add=True, db_index=True)
        updated_at = models.DateTimeField("수정일", auto_now=True, db_index=True)
    
        class Meta:
            verbose_name = "리뷰이미지"
            verbose_name_plural = "리뷰이미지"
    
        def __str__(self):
            return f"{self.id}:{self.image}"
    

    SocialChannel (소셜채널)
    필드 이름 데이터 타입 한글 필드명 옵션
    id AutoField (PK)
    name CharField(max_length=100) 이름
    관계
    • Review.social_channel ← 1:N

    역할: 리뷰가 어디에서 왔는지(인스타·블로그·유튜브 등) 분류 관리 기능:

    • 채널 명칭 추가·삭제
    • 리뷰 등록 시 드롭다운으로 선택
    class SocialChannel(models.Model):
        name = models.CharField("이름", max_length=100)
    
        class Meta:
            verbose_name = "소셜채널"
            verbose_name_plural = "소셜채널"
    
        def __str__(self):
            return self.name
    

    Tag (태그)
    필드 이름 데이터 타입 한글 필드명 옵션
    id AutoField (PK)
    name CharField(max_length=100) 이름
    • 관계
      • Restaurant.tags ← M:N (implicit through table 생성)

    역할: “가성비 좋은”, “웨이팅 필수”, “야외 좌석” 등 자유로운 키워드 태그 관리 기능:

    • 태그 집합 관리 → 레스토랑별 복수 선택
    • 사이트 내 태그 필터·추천 로직에 활용
    class Tag(models.Model):
        name = models.CharField("이름", max_length=100)
    
        class Meta:
            verbose_name = "태그"
            verbose_name_plural = "태그"
    
        def __str__(self):
            return self.name
    

    Region (지역)
    필드 이름 데이터 타입 한글 필드명 옵션
    id AutoField (PK) 자동 생성
    sido CharField(max_length=20) 광역시도 중복 허용, 인덱스 권장
    sigungu CharField(max_length=20) 시군구 중복 허용, 인덱스 권장
    eupmyeondong CharField(max_length=20) 읍면동 중복 허용, 인덱스 권장
    제약 조건
    • unique_together = ("sido", "sigungu", "eupmyeondong")
      동일한 지역 정보 중복 저장 방지 관계
    • Restaurant.regionM:1 (ForeignKey)
      하나의 레스토랑은 하나의 지역에 속함
      하나의 지역에는 여러 레스토랑이 존재 가능

    역할:

    • 레스토랑의 주소 정규화를 위한 기본 테이블
    • 광역시도/시군구/읍면동 단위로 관리되며, 레스토랑의 위치 기반 정보 제공
    • 지역 필터, 검색, 추천 알고리즘, 랭킹 등 다양한 기능의 기반 데이터로 활용

    관리 기능:

    • 사전 정의 또는 외부 행정구역 API 기반 자동 생성 가능
    • 관리자 페이지에서 중복 없이 지역 리스트 관리
    • 유저가 지역을 기반으로 맛집을 탐색하거나, 지역 랭킹 등 통계 기능에서 활용 가능
    class Region(models.Model):
        sido = models.CharField("광역시도", max_length=20)
        sigungu = models.CharField("시군구", max_length=20)
        eupmyeondong = models.CharField("읍면동", max_length=20)
    
        class Meta:
            verbose_name = "지역"
            verbose_name_plural = "지역"
            unique_together = ("sido", "sigungu", "eupmyeondong")
    
        def __str__(self):
            return f"{self.sido} {self.sigungu} {self.eupmyeondong}"
    

    모델 전체코드

    models.py 생성

    from django.core.validators import MaxValueValidator, MinValueValidator
    from django.db import models
    from django.forms import ValidationError
    
    class Article(models.Model):
        title = models.CharField(max_length=100, db_index=True)
        preview_image = models.ImageField(upload_to="article", null=True, blank=True)
        content = models.TextField()
        show_at_index = models.BooleanField(default=False)
        is_published = models.BooleanField(default=False)
        created_at = models.DateTimeField("생성일", auto_now_add=True)
        modified_at = models.DateTimeField(auto_now=True)
    
        class Meta:
            verbose_name = "칼럼"
            verbose_name_plural = "칼럼"
    
        def __str__(self):
            return f"{self.id} - {self.title}"
    
    class Restaurant(models.Model):
        name = models.CharField("이름", max_length=100, db_index=True)
        branch_name = models.CharField(
            "지점", max_length=100, db_index=True, null=True, blank=True
        )
        description = models.TextField("설명", null=True, blank=True)
        address = models.CharField("주소", max_length=255, db_index=True)
        feature = models.CharField("특징", max_length=255)
        is_closed = models.BooleanField("폐업 여부", default=False)
        latitude = models.DecimalField(
            "위도",
            max_digits=16,
            decimal_places=12,
            db_index=True,
            default="0.0000",
        )
        longitude = models.DecimalField(
            "경도",
            max_digits=16,
            decimal_places=12,
            db_index=True,
            default="0.0000",
        )
        phone = models.CharField(
            "전화번호", max_length=16, help_text="E.164 포맷", blank=True, null=True
        )
        rating = models.DecimalField("평점", max_digits=3, decimal_places=2, default="0.0")
        rating_count = models.PositiveIntegerField("평가수", default=0)
        start_time = models.TimeField("영업 시작 시간", null=True, blank=True)
        end_time = models.TimeField("영업 종료 시간", null=True, blank=True)
        last_order_time = models.TimeField("라스트 오더 시간", null=True, blank=True)
        category = models.ForeignKey(
            "RestaurantCategory", on_delete=models.SET_NULL, blank=True, null=True
        )
        tags = models.ManyToManyField("Tag", blank=True)
        region = models.ForeignKey(
            "Region",
            on_delete=models.SET_NULL,
            null=True,
            blank=True,
            verbose_name="지역",
            related_name="restaurants",
        )
    
        class Meta:
            verbose_name = "레스토랑"
            verbose_name_plural = "레스토랑"
    
        def __str__(self):
            return f"{self.name} {self.branch_name}" if self.branch_name else f"{self.name}"
    
    class CuisineType(models.Model):
        name = models.CharField("이름", max_length=20)
    
        class Meta:
            verbose_name = "음식 종류"
            verbose_name_plural = "음식 종류"
    
        def __str__(self):
            return self.name
    
    class RestaurantCategory(models.Model):
        name = models.CharField("이름", max_length=20)
        cuisine_type = models.ForeignKey(
            "CuisineType",
            on_delete=models.CASCADE,
            null=True,
            blank=True,
        )
    
        class Meta:
            verbose_name = "가게 카테고리"
            verbose_name_plural = "가게 카테고리"
    
        def __str__(self):
            return self.name
    
    class RestaurantImage(models.Model):
        restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
        is_representative = models.BooleanField("대표 이미지 여부", default=False)
        order = models.PositiveIntegerField("순서", null=True, blank=True)
        name = models.CharField("이름", max_length=100, null=True, blank=True)
        image = models.ImageField("이미지", max_length=100, upload_to="restaurant")
        created_at = models.DateTimeField("생성일", auto_now_add=True, db_index=True)
        updated_at = models.DateTimeField("수정일", auto_now=True, db_index=True)
    
        class Meta:
            verbose_name = "가게 이미지"
            verbose_name_plural = "가게 이미지"
    
        def __str__(self):
            return f"{self.id}:{self.image}"
    
        def clean(self):
            images = self.restaurant.restaurantimage_set.filter(is_representative=True)
            if self.is_representative and images.exclude(id=self.id).count() > 0:
                raise ValidationError("대표 이미지는 1개만 지정 가능합니다.")
    
    class RestaurantMenu(models.Model):
        restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
        name = models.CharField("이름", max_length=100)
        price = models.PositiveIntegerField("가격", default=0)
        image = models.ImageField(
            "이미지", upload_to="restaurant-menu", null=True, blank=True
        )
        created_at = models.DateTimeField("생성일", auto_now_add=True, db_index=True)
        updated_at = models.DateTimeField("수정일", auto_now=True, db_index=True)
    
        class Meta:
            verbose_name = "가게 메뉴"
            verbose_name_plural = "가게 메뉴"
    
        def __str__(self):
            return self.name
    
    class Review(models.Model):
        title = models.CharField("제목", max_length=100)
        author = models.CharField("작성자", max_length=100)
        profile_image = models.ImageField(
            "프로필 이미지", upload_to="review-profile", blank=True, null=True
        )
        content = models.TextField("내용")
        rating = models.PositiveSmallIntegerField(
            validators=[MinValueValidator(1), MaxValueValidator(5)]
        )
        restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
        social_channel = models.ForeignKey(
            "SocialChannel", on_delete=models.SET_NULL, blank=True, null=True
        )
        created_at = models.DateTimeField("생성일", auto_now_add=True, db_index=True)
        updated_at = models.DateTimeField("수정일", auto_now=True, db_index=True)
    
        class Meta:
            verbose_name = "리뷰"
            verbose_name_plural = "리뷰"
            ordering = ["-created_at"]
    
        def __str__(self):
            return f"{self.author}:{self.title}"
    
        @property
        def restaurant_name(self):
            return self.restaurant.name
    
        @property
        def content_partial(self):
            return self.content[:20]
    
    class ReviewImage(models.Model):
        review = models.ForeignKey(Review, on_delete=models.CASCADE)
        name = models.CharField(max_length=100)
        image = models.ImageField(max_length=100, upload_to="review")
        created_at = models.DateTimeField("생성일", auto_now_add=True, db_index=True)
        updated_at = models.DateTimeField("수정일", auto_now=True, db_index=True)
    
        class Meta:
            verbose_name = "리뷰이미지"
            verbose_name_plural = "리뷰이미지"
    
        def __str__(self):
            return f"{self.id}:{self.image}"
    
    class SocialChannel(models.Model):
        name = models.CharField("이름", max_length=100)
    
        class Meta:
            verbose_name = "소셜채널"
            verbose_name_plural = "소셜채널"
    
        def __str__(self):
            return self.name
    
    class Tag(models.Model):
        name = models.CharField("이름", max_length=100)
    
        class Meta:
            verbose_name = "태그"
            verbose_name_plural = "태그"
    
        def __str__(self):
            return self.name
    
    class Region(models.Model):
        sido = models.CharField("광역시도", max_length=20)
        sigungu = models.CharField("시군구", max_length=20)
        eupmyeondong = models.CharField("읍면동", max_length=20)
    
        class Meta:
            verbose_name = "지역"
            verbose_name_plural = "지역"
            unique_together = ("sido", "sigungu", "eupmyeondong")
    
        def __str__(self):
            return f"{self.sido} {self.sigungu} {self.eupmyeondong}"
    

    초기 마이그레이션 및 관리자 생성

    python manage.py makemigrations
    python manage.py migrate
    python manage.py createsuperuser
    

    github에서 action작업을 했으므로 vscode에서는 병합작업을 통해 Merge를 시켜야 합니다.

    Mage방식으로 병합

    git config --global pull.rebase false 
    # 병합 전략을 'merge' 방식으로 설정 (병합 설정은 1회만 하면 됨)
    
    git pull origin main # 원격 브랜치(main)의 변경사항을 로컬에 병합
    
    git config --global --get pull.rebase 
    # 설정 확인 (false면 merge 방식)
    

    잘 병합됐는지 확인하려면:

    git log --oneline
    git status
    
    TOP
    preload preload