Skip to content

Commit ffa69bc

Browse files
committed
feat: Product의 image를 PublicFile로 마이그레이션
1 parent 6ef0b13 commit ffa69bc

6 files changed

Lines changed: 102 additions & 3 deletions

File tree

app/admin_api/serializers/shop/products.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
NestedFieldSpec,
88
NestedModelSerializer,
99
)
10+
from file.models import PublicFile
1011
from rest_framework import serializers
1112
from shop.product.models import Category, CategoryGroup, Option, OptionGroup, Product, Tag
1213

@@ -97,6 +98,11 @@ def validate(self, attrs: dict) -> dict:
9798
class ProductAdminSerializer(BaseAbstractSerializer, JsonSchemaSerializer, serializers.ModelSerializer):
9899
option_groups = OptionGroupAdminSerializer(many=True, read_only=True)
99100
tag_set = serializers.PrimaryKeyRelatedField(many=True, queryset=Tag.objects.filter_active(), required=False)
101+
image = serializers.PrimaryKeyRelatedField(
102+
queryset=PublicFile.objects.filter_active(),
103+
allow_null=True,
104+
required=False,
105+
)
100106

101107
class Meta:
102108
model = Product

app/admin_api/views/shop/products.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class ProductAdminViewSet(JsonSchemaViewSet, viewsets.ModelViewSet):
4545
permission_classes = [IsSuperUser]
4646
queryset = (
4747
Product.objects.filter_active()
48-
.select_related_with_user("category", "category__group")
48+
.select_related_with_user("category", "category__group", "image")
4949
.prefetch_related(
5050
Prefetch("tags", queryset=ProductTagRelation.objects.filter_active().select_related("tag")),
5151
Prefetch(
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import django.db.models.deletion
2+
from django.db import migrations, models
3+
4+
# 사전 계산값. 새 URL 이 발견되면 여기에 추가하거나 RunPython 결과를 확인할 것.
5+
LEGACY_IMAGE_META: dict[str, dict] = {
6+
"https://s3.ap-northeast-2.amazonaws.com/pyconkr-backend-prod-public/public/t-shirt-compressed.png": {
7+
"file_path": "public/t-shirt-compressed.png",
8+
"hash": "d131452cf6cd2287e4c302f4f7c17bb5",
9+
"size": 2069683,
10+
"mimetype": "image/png",
11+
},
12+
"https://s3.ap-northeast-2.amazonaws.com/pyconkr-backend-prod-public/public/t-shirt-comporessed-2.png": {
13+
"file_path": "public/t-shirt-comporessed-2.png",
14+
"hash": "30acab0125661cac1ce75c4a4633042a",
15+
"size": 2278886,
16+
"mimetype": "image/png",
17+
},
18+
}
19+
20+
21+
def _migrate_image_urls(apps, schema_editor) -> None:
22+
Product = apps.get_model("product", "Product")
23+
PublicFile = apps.get_model("file", "PublicFile")
24+
25+
unknown_urls: list[tuple[str, str]] = []
26+
for product in Product.objects.exclude(image__isnull=True).exclude(image="").iterator():
27+
url = product.image
28+
meta = LEGACY_IMAGE_META.get(url)
29+
if not meta:
30+
unknown_urls.append((str(product.id), url))
31+
continue
32+
public_file, _ = PublicFile.objects.get_or_create(
33+
file=meta["file_path"],
34+
defaults={"hash": meta["hash"], "size": meta["size"], "mimetype": meta["mimetype"]},
35+
)
36+
product.image_publicfile = public_file
37+
product.save(update_fields=["image_publicfile"])
38+
39+
if unknown_urls:
40+
# 끊은 채로 진행 (admin 수동 정정 가능) — 단, 잊지 않도록 stdout 로 알림.
41+
print(f"[migration product.0002] {len(unknown_urls)} unknown image URL(s) — image FK left NULL: {unknown_urls}")
42+
43+
44+
def _noop_reverse(apps, schema_editor) -> None:
45+
pass
46+
47+
48+
class Migration(migrations.Migration):
49+
dependencies = [("file", "0001_initial"), ("product", "0001_initial")]
50+
operations = [
51+
# 1) 신규 FK 컬럼 추가
52+
migrations.AddField(
53+
model_name="product",
54+
name="image_publicfile",
55+
field=models.ForeignKey(
56+
blank=True,
57+
null=True,
58+
on_delete=django.db.models.deletion.PROTECT,
59+
related_name="+",
60+
to="file.publicfile",
61+
verbose_name="대표 이미지",
62+
),
63+
),
64+
migrations.AddField(
65+
model_name="historicalproduct",
66+
name="image_publicfile",
67+
field=models.ForeignKey(
68+
blank=True,
69+
db_constraint=False,
70+
null=True,
71+
on_delete=django.db.models.deletion.DO_NOTHING,
72+
related_name="+",
73+
to="file.publicfile",
74+
verbose_name="대표 이미지",
75+
),
76+
),
77+
# 2) URL → PublicFile 매핑
78+
migrations.RunPython(_migrate_image_urls, _noop_reverse),
79+
# 3) 구 URLField 컬럼 제거
80+
migrations.RemoveField(model_name="product", name="image"),
81+
migrations.RemoveField(model_name="historicalproduct", name="image"),
82+
# 4) image_publicfile → image
83+
migrations.RenameField(model_name="product", old_name="image_publicfile", new_name="image"),
84+
migrations.RenameField(model_name="historicalproduct", old_name="image_publicfile", new_name="image"),
85+
]

app/shop/product/models.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,14 @@ def get_user_taken_stock_count(
106106
class Product(BaseAbstractModel):
107107
name = models.TextField()
108108
description = models.TextField(null=True, blank=True)
109-
image = models.URLField(null=True, blank=True)
109+
image = models.ForeignKey(
110+
"file.PublicFile",
111+
on_delete=models.PROTECT,
112+
null=True,
113+
blank=True,
114+
related_name="+",
115+
verbose_name="대표 이미지",
116+
)
110117

111118
price = models.PositiveIntegerField()
112119
stock = models.IntegerField(default=0)

app/shop/product/serializers/dto.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class Meta:
3333
class ProductDto(serializers.ModelSerializer):
3434
category_group = serializers.CharField(source="category.group.name")
3535
category = serializers.CharField(source="category.name")
36+
image = serializers.FileField(source="image.file", read_only=True, allow_null=True)
3637
option_groups = OptionGroupDto(many=True)
3738
tag_names: serializers.StringRelatedField = serializers.StringRelatedField(source="tags", many=True)
3839

app/shop/product/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def get_queryset(self) -> QuerySet[Product]:
5959
return (
6060
Product.objects.filter_active()
6161
.filter(filter)
62-
.select_related("category", "category__group")
62+
.select_related("category", "category__group", "image")
6363
.prefetch_related(
6464
"tags",
6565
Prefetch("option_groups", queryset=OptionGroup.objects.prefetch_related("options")),

0 commit comments

Comments
 (0)