로딩 중이에요... 🐣
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.region
← M: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