[fix] solution: 썸네일 저장소 없이도 내 사이트·랜딩 카드에 그림을 채운다
Azure 썸네일 저장소가 안 꺼져 있으면(로컬 개발 등) sites.thumbnail_url 이 계속 비어 있어 발행된 사이트도 카드가 아이콘으로 떨어졌다. 그 대신 빌더가 이미 쓰는 대표 사진(place_photos)을 한 번이라도 발행한 줄에 채운다 — 내 사이트 목록과 랜딩 쇼케이스 둘 다 같은 규칙. EssentialInfoSection 예약 공지 라벨에서 채널명(NOL)을 뺀다.
This commit is contained in:
parent
a6ddeccdff
commit
217d0853bc
@ -5,12 +5,34 @@ from sqlalchemy import and_, func, select, update
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from common.database.db_session_manager import DB_SESSION_MNG
|
from common.database.db_session_manager import DB_SESSION_MNG
|
||||||
from common.database.model.models import places, site_publish_logs, site_versions, sites
|
from common.database.model.models import place_photos, places, site_publish_logs, site_versions, sites
|
||||||
from common.enums import BuildStatus, ErrorType, SiteStatus
|
from common.enums import BuildStatus, ErrorType, MediaStatus, SiteStatus
|
||||||
from common.logger import LOG
|
from common.logger import LOG
|
||||||
from common.utils.gtime import GTime
|
from common.utils.gtime import GTime
|
||||||
|
|
||||||
|
|
||||||
|
def _primary_photo_subquery():
|
||||||
|
"""place_photos 에서 대표 사진 한 장의 url 만 고르는 상관 서브쿼리(사업장당 1행).
|
||||||
|
|
||||||
|
site_payload.primary_media 와 같은 규칙 — 객실·메뉴 사진(unit_id 있음)이 아닌 첫 장,
|
||||||
|
sort_order 순. `.correlate(places)` 라서 바깥 쿼리가 `places` 를 셀렉트에 들고 있어야 한다.
|
||||||
|
sites.thumbnail_url 이 비어 있을 때(Azure 썸네일 저장소 미설정 등) 서비스 계층이 이걸로
|
||||||
|
대신 채운다 — 여기서는 후보만 얹고, 언제 쓸지는 서비스 계층 몫이다."""
|
||||||
|
return (
|
||||||
|
select(place_photos.url)
|
||||||
|
.where(
|
||||||
|
place_photos.place_id == places.place_id,
|
||||||
|
place_photos.deleted == False, # noqa: E712
|
||||||
|
place_photos.status == MediaStatus.APPROVED.value,
|
||||||
|
place_photos.unit_id.is_(None),
|
||||||
|
)
|
||||||
|
.order_by(place_photos.sort_order.asc(), place_photos.created_at.asc())
|
||||||
|
.limit(1)
|
||||||
|
.correlate(places)
|
||||||
|
.scalar_subquery()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# 사이트/버전/발행로그 CRUD. 항상 place_id 또는 site_id 로 스코프한다.
|
# 사이트/버전/발행로그 CRUD. 항상 place_id 또는 site_id 로 스코프한다.
|
||||||
class ISiteCRUD(ABC):
|
class ISiteCRUD(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -23,6 +45,7 @@ class ISiteCRUD(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def list_owner_sites(self, cdb: AsyncSession, owner_user_id, skip, limit) -> Tuple[ErrorType, list, int]:
|
async def list_owner_sites(self, cdb: AsyncSession, owner_user_id, skip, limit) -> Tuple[ErrorType, list, int]:
|
||||||
|
"""(ErrorType, [(place, site, built_at, primary_photo_url)], 총건수)."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -67,6 +90,7 @@ class ISiteCRUD(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def list_published(self, cdb: AsyncSession, limit: int) -> Tuple[ErrorType, list]:
|
async def list_published(self, cdb: AsyncSession, limit: int) -> Tuple[ErrorType, list]:
|
||||||
|
"""(ErrorType, [(site, place, primary_photo_url)])."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -98,10 +122,16 @@ class SiteCRUD(ISiteCRUD):
|
|||||||
return ErrorType.DB_RUN_FAILED, None
|
return ErrorType.DB_RUN_FAILED, None
|
||||||
|
|
||||||
async def list_owner_sites(self, cdb: AsyncSession, owner_user_id, skip: int, limit: int) -> Tuple[ErrorType, list, int]:
|
async def list_owner_sites(self, cdb: AsyncSession, owner_user_id, skip: int, limit: int) -> Tuple[ErrorType, list, int]:
|
||||||
"""사장님의 사업장 + 사이트 + 마지막 빌드 시각. (ErrorType, [(place, site, built_at)], 총건수).
|
"""사장님의 사업장 + 사이트 + 마지막 빌드 시각 + 빌더 대표 사진.
|
||||||
|
(ErrorType, [(place, site, built_at, primary_photo_url)], 총건수).
|
||||||
|
|
||||||
따로 읽으면 줄마다 사이트를 다시 물어 N+1 이다. LEFT JOIN 이라 사이트가 없는 사업장
|
따로 읽으면 줄마다 사이트를 다시 물어 N+1 이다. LEFT JOIN 이라 사이트가 없는 사업장
|
||||||
(위저드만 걸어온 것)도 내려간다 — 빠지면 만들다 만 것을 찾을 길이 없다."""
|
(위저드만 걸어온 것)도 내려간다 — 빠지면 만들다 만 것을 찾을 길이 없다.
|
||||||
|
|
||||||
|
★ primary_photo_url 은 site_payload.primary_media 와 같은 규칙(사진 중 객실·메뉴가 아닌
|
||||||
|
첫 장, sort_order 순)으로 고른 place_photos.url 이다 — sites.thumbnail_url 이 비어 있을 때
|
||||||
|
(Azure 썸네일 저장소 미설정 등으로 재호스팅에 실패한 경우) 서비스 계층이 이걸로 대신 채운다.
|
||||||
|
여기서는 후보만 얹고, "발행한 적 있는 줄에만 쓴다"는 판단은 서비스 계층 몫이다."""
|
||||||
try:
|
try:
|
||||||
where = and_(places.deleted == False, places.owner_user_id == owner_user_id) # noqa: E712
|
where = and_(places.deleted == False, places.owner_user_id == owner_user_id) # noqa: E712
|
||||||
|
|
||||||
@ -111,7 +141,7 @@ class SiteCRUD(ISiteCRUD):
|
|||||||
total = int(cnt_rows[0] or 0) if cnt_rows else 0
|
total = int(cnt_rows[0] or 0) if cnt_rows else 0
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
select(places, sites, site_versions.built_at)
|
select(places, sites, site_versions.built_at, _primary_photo_subquery())
|
||||||
.outerjoin(sites, and_(sites.place_id == places.place_id, sites.deleted == False)) # noqa: E712
|
.outerjoin(sites, and_(sites.place_id == places.place_id, sites.deleted == False)) # noqa: E712
|
||||||
.outerjoin(site_versions, site_versions.site_version_id == sites.current_version_id)
|
.outerjoin(site_versions, site_versions.site_version_id == sites.current_version_id)
|
||||||
.where(where)
|
.where(where)
|
||||||
@ -265,14 +295,18 @@ class SiteCRUD(ISiteCRUD):
|
|||||||
return ErrorType.DB_RUN_FAILED, []
|
return ErrorType.DB_RUN_FAILED, []
|
||||||
|
|
||||||
async def list_published(self, cdb: AsyncSession, limit: int = 12) -> Tuple[ErrorType, list]:
|
async def list_published(self, cdb: AsyncSession, limit: int = 12) -> Tuple[ErrorType, list]:
|
||||||
"""발행된 사이트 + 그 사업장을 최신순으로. 랜딩 쇼케이스가 읽는 목록이다.
|
"""발행된 사이트 + 그 사업장 + 빌더 대표 사진을 최신순으로. 랜딩 쇼케이스가 읽는 목록이다.
|
||||||
|
(ErrorType, [(site, place, primary_photo_url)]).
|
||||||
|
|
||||||
★ 회사 스코프가 없는 **유일한** 사이트 조회다(비로그인 API 가 쓴다). 그래서 행을 통째로
|
★ 회사 스코프가 없는 **유일한** 사이트 조회다(비로그인 API 가 쓴다). 그래서 행을 통째로
|
||||||
돌려주고, 무엇이 밖으로 나갈지는 services/showcase_service 한 곳에서만 고른다 —
|
돌려주고, 무엇이 밖으로 나갈지는 services/showcase_service 한 곳에서만 고른다 —
|
||||||
여기서 열을 골라 두면 나중에 필드를 늘릴 때 공개 여부를 판단할 자리가 사라진다."""
|
여기서 열을 골라 두면 나중에 필드를 늘릴 때 공개 여부를 판단할 자리가 사라진다.
|
||||||
|
|
||||||
|
★ primary_photo_url 은 list_owner_sites 와 같은 서브쿼리(_primary_photo_subquery) —
|
||||||
|
sites.thumbnail_url 이 비어 있을 때 showcase_service 가 이걸로 대신 채운다."""
|
||||||
try:
|
try:
|
||||||
query = (
|
query = (
|
||||||
select(sites, places)
|
select(sites, places, _primary_photo_subquery())
|
||||||
.join(places, places.place_id == sites.place_id)
|
.join(places, places.place_id == sites.place_id)
|
||||||
.where(
|
.where(
|
||||||
sites.status == SiteStatus.PUBLISHED.value,
|
sites.status == SiteStatus.PUBLISHED.value,
|
||||||
|
|||||||
@ -97,6 +97,8 @@ class MySiteData(WebPacketProtocol):
|
|||||||
template_id: Optional[str] = None
|
template_id: Optional[str] = None
|
||||||
published_at: Optional[datetime] = None
|
published_at: Optional[datetime] = None
|
||||||
# 목록 카드의 그림. 발행에 성공해야 채워지고, 발행마다 `?v=` 가 바뀐다(site_thumbnail.public_url).
|
# 목록 카드의 그림. 발행에 성공해야 채워지고, 발행마다 `?v=` 가 바뀐다(site_thumbnail.public_url).
|
||||||
|
# Azure 썸네일 저장소가 안 꺼져 있으면(로컬 개발) 빌더가 쓰는 대표 사진으로 대신 채운다
|
||||||
|
# (site_service._my_site_row) — 이때는 `?v=` 가 없다.
|
||||||
thumbnail_url: Optional[str] = None
|
thumbnail_url: Optional[str] = None
|
||||||
# 단건과 같은 규칙 — 노출값이 마지막 빌드보다 나중에 바뀌었으면 재발행 대상이다.
|
# 단건과 같은 규칙 — 노출값이 마지막 빌드보다 나중에 바뀌었으면 재발행 대상이다.
|
||||||
needs_rebuild: bool = False
|
needs_rebuild: bool = False
|
||||||
|
|||||||
@ -37,8 +37,11 @@ class ShowcaseService:
|
|||||||
# ★ 주소 규칙은 site_payload 한 곳뿐이다 — 여기서 다시 만들면
|
# ★ 주소 규칙은 site_payload 한 곳뿐이다 — 여기서 다시 만들면
|
||||||
# 카드가 가리키는 곳과 실제 발행 주소가 갈린다(CLAUDE.md '슬러그 규칙은 두 곳').
|
# 카드가 가리키는 곳과 실제 발행 주소가 갈린다(CLAUDE.md '슬러그 규칙은 두 곳').
|
||||||
url=f"/s/{site_payload.publish_slug(place, site)}",
|
url=f"/s/{site_payload.publish_slug(place, site)}",
|
||||||
thumbnail_url=site.thumbnail_url,
|
# ★ sites.thumbnail_url 은 Azure 썸네일 저장소가 꺼져 있으면(로컬 개발 등) 비어
|
||||||
|
# 있다 — 이 목록은 이미 PUBLISHED 만 걷었으므로, 그때는 빌더가 쓰는 대표 사진
|
||||||
|
# (primary_photo_url)으로 대신 채운다(services/site_service._my_site_row 와 같은 규칙).
|
||||||
|
thumbnail_url=site.thumbnail_url or primary_photo_url,
|
||||||
)
|
)
|
||||||
for site, place in rows
|
for site, place, primary_photo_url in rows
|
||||||
]
|
]
|
||||||
return res
|
return res
|
||||||
|
|||||||
@ -460,14 +460,24 @@ class SiteService:
|
|||||||
if err_type != ErrorType.SUCCESS:
|
if err_type != ErrorType.SUCCESS:
|
||||||
res.result.SetResult(err_type)
|
res.result.SetResult(err_type)
|
||||||
return res
|
return res
|
||||||
res.sites = [self._my_site_row(place, site, built_at) for place, site, built_at in rows]
|
res.sites = [
|
||||||
|
self._my_site_row(place, site, built_at, primary_photo_url)
|
||||||
|
for place, site, built_at, primary_photo_url in rows
|
||||||
|
]
|
||||||
res.total = total
|
res.total = total
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _my_site_row(place, site, built_at) -> MySiteData:
|
def _my_site_row(place, site, built_at, primary_photo_url) -> MySiteData:
|
||||||
# ★ 재빌드 판별은 단건(get_site)과 같은 규칙이어야 한다 — 다르면 목록과 에디터가 다른 답을 한다.
|
# ★ 재빌드 판별은 단건(get_site)과 같은 규칙이어야 한다 — 다르면 목록과 에디터가 다른 답을 한다.
|
||||||
changed = place.content_updated_at
|
changed = place.content_updated_at
|
||||||
|
# ★ sites.thumbnail_url 은 발행할 때 Azure 에 대표 사진을 재호스팅해야 채워진다
|
||||||
|
# (services/site_thumbnail.store) — 저장소가 안 꺼져 있으면(로컬 개발 등) 늘 비어 있다.
|
||||||
|
# 그래도 "한 번이라도 발행한 줄은 그림"이라는 화면 규칙은 지켜야 하므로, 빌더가 이미
|
||||||
|
# 쓰고 있는 대표 사진(place_photos, primary_photo_url)으로 대신 채운다 — 발행 전 줄에는
|
||||||
|
# 쓰지 않는다(그 규칙은 published_at 유무로 가른다: crud.site_crud.list_owner_sites).
|
||||||
|
ever_published = site is not None and getattr(site, "published_at", None) is not None
|
||||||
|
thumbnail_url = getattr(site, "thumbnail_url", None) or (primary_photo_url if ever_published else None)
|
||||||
return MySiteData(
|
return MySiteData(
|
||||||
place_id=place.place_id,
|
place_id=place.place_id,
|
||||||
name=place.name,
|
name=place.name,
|
||||||
@ -480,7 +490,7 @@ class SiteService:
|
|||||||
domain=getattr(site, "domain", None),
|
domain=getattr(site, "domain", None),
|
||||||
template_id=getattr(site, "template_id", None),
|
template_id=getattr(site, "template_id", None),
|
||||||
published_at=getattr(site, "published_at", None),
|
published_at=getattr(site, "published_at", None),
|
||||||
thumbnail_url=getattr(site, "thumbnail_url", None),
|
thumbnail_url=thumbnail_url,
|
||||||
needs_rebuild=bool(site is not None and changed and (built_at is None or changed > built_at)),
|
needs_rebuild=bool(site is not None and changed and (built_at is None or changed > built_at)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -113,7 +113,7 @@ export function EssentialInfoSection() {
|
|||||||
{otherRows.length > 0 && <Rows title="시설 · 편의" rows={otherRows} />}
|
{otherRows.length > 0 && <Rows title="시설 · 편의" rows={otherRows} />}
|
||||||
{guides.filter((link) => link.stayGuide?.reservation).map((link) => (
|
{guides.filter((link) => link.stayGuide?.reservation).map((link) => (
|
||||||
<Rows key={`reservation-${link.url}`} title="예약 공지" emphasis rows={[
|
<Rows key={`reservation-${link.url}`} title="예약 공지" emphasis rows={[
|
||||||
{label: 'NOL 예약 공지', value: link.stayGuide?.reservation ?? ''},
|
{label: '예약 공지', value: link.stayGuide?.reservation ?? ''},
|
||||||
]} />
|
]} />
|
||||||
))}
|
))}
|
||||||
<BookingRow />
|
<BookingRow />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user