인앱 미니 블로그(AI 자동 포스트, 이메일 승인)·이용후기(즉시 게시)·예약 요청(메일 발송)을
새로 붙였고, 병행해서 /s/stay 목업과 발행 사이트 공통 렌더러(UnitsSection·FestivalSection·
LocalGuideSection·WeatherSection 등)의 UI 버그를 다수 고쳤다. 범위가 넓지만 한 주 분량
작업을 한 커밋으로 묶어 달라는 요청에 따라 하나로 묶는다.
- solution/backend: post/review/booking_request 라우터·서비스·CRUD 추가, 스케줄러에
블로그 초안 생성(새벽 4:10)·발송(아침 9:00) cron 등록, 마이그레이션 4건 추가
- solution/frontend, admin/frontend: 생성된 API 클라이언트 갱신, 리뷰 모더레이션·
블로그 글 관리 페이지 추가
- solution/site/src: 객실 상세+실시간예약(날짜선택·연락처 폼)을 모달로 통합, 축제·
주변안내 카드 클릭 시 모달 전환, 후기 목록 카드 UI, 공용 Modal 컴포넌트 신설,
날씨 문구 동기화 버그 수정(하늘줄·기온줄 한 타이머로), 시설·편의 가능/불가 아이콘
색상 하이라이트, 헤더 메뉴 순서를 실제 섹션 순서에 맞춤, 하단 탭바 아이콘 정렬 버그
(line-height) 수정, 추천일정 점선 연결+데스크톱 자동펼침/모바일 축소, 채널 라벨에
크롤링 원문("NOL")이 새던 것을 bookingLabel() 로 교체
- solution/site/scripts/mockup: /s/stay 패치 스크립트·주입 CSS·JS 다수 수정, stay4~6
빌드 스크립트 추가(다른 세션 작업)
테스트: solution/site `npx tsc --noEmit` 통과, `npx vitest run` 93 passed,
solution/backend `pytest tests/test_booking_request.py` 6 passed(로컬 DB 대상).
예약 요청 메일은 실제 발송까지 확인(place 66894a1b 소유자 이메일 누락을 DB에서 보정).
87 lines
3.6 KiB
Python
87 lines
3.6 KiB
Python
"""예약 요청 메일 — 발행본 폼이 보낸 것을 사장님에게 전달한다.
|
|
|
|
★ 저장하지 않는다. place_id 로 받는 사람만 찾고, 나머지는 전부 메일 본문으로 나간다.
|
|
★ 발행된 사이트의 업장만 받는다 — place_id 를 손으로 바꿔 아무 업장에나 메일을 쏘는 길을 막는다.
|
|
"""
|
|
from sqlalchemy import select
|
|
|
|
from common.database.db_session_manager import DB_SESSION_MNG
|
|
from common.database.model.models import places, sites, users
|
|
from common.enums import DBWRType, SiteStatus
|
|
from common.logger import LOG
|
|
from services import mail_service
|
|
|
|
NOT_AVAILABLE = "지금은 온라인 예약 요청을 받을 수 없습니다. 전화로 문의해 주세요."
|
|
SENT = "예약 요청을 보냈습니다. 사장님이 확인 후 연락드립니다."
|
|
|
|
|
|
class BookingRequestService:
|
|
async def send(self, body):
|
|
from router.v1.site.booking_request import ResBookingRequest
|
|
|
|
target = await self._target(body.place_id)
|
|
if not target:
|
|
return ResBookingRequest(success=False, message=NOT_AVAILABLE)
|
|
|
|
place_name, owner_email = target
|
|
if not mail_service.is_configured() or not mail_service.is_valid_address(owner_email):
|
|
LOG.w("[booking-request] 받는 주소나 SMTP 설정이 없어 전달하지 못했다")
|
|
return ResBookingRequest(success=False, message=NOT_AVAILABLE)
|
|
|
|
reply_to = body.email if body.email and mail_service.is_valid_address(body.email) else None
|
|
ok = mail_service.send(
|
|
to=owner_email,
|
|
subject=f"[예약 요청] {place_name} — {body.name} 님",
|
|
text=_body(place_name, body),
|
|
reply_to=reply_to,
|
|
)
|
|
if not ok:
|
|
return ResBookingRequest(success=False, message=NOT_AVAILABLE)
|
|
|
|
LOG.i(f"[booking-request] 전달 완료 place={body.place_id}")
|
|
return ResBookingRequest(success=True, message=SENT)
|
|
|
|
async def _target(self, place_id) -> tuple[str, str] | None:
|
|
"""(상호명, 사장님 이메일). 발행된 사이트가 없으면 None."""
|
|
def query(session):
|
|
return session.execute(
|
|
select(places.name, users.email)
|
|
.join(sites, sites.place_id == places.place_id)
|
|
.join(users, users.user_id == places.owner_user_id)
|
|
.where(
|
|
places.place_id == place_id,
|
|
places.deleted == False, # noqa: E712
|
|
sites.deleted == False, # noqa: E712
|
|
sites.status == SiteStatus.PUBLISHED.value,
|
|
)
|
|
)
|
|
|
|
result = await DB_SESSION_MNG.execute_lambda(places.DBType(), DBWRType.DB_READ.value, query)
|
|
row = result.first() if result is not None else None
|
|
if not row or not row[1]:
|
|
return None
|
|
return str(row[0] or ""), str(row[1])
|
|
|
|
|
|
def _body(place_name: str, body) -> str:
|
|
lines = [
|
|
f"{place_name} 예약 요청이 도착했습니다.",
|
|
"",
|
|
f"성함 {body.name}",
|
|
f"연락처 {body.phone}",
|
|
]
|
|
if body.email:
|
|
lines.append(f"이메일 {body.email}")
|
|
if body.stay:
|
|
lines.append(f"일정 {body.stay}")
|
|
if body.guests:
|
|
lines.append(f"인원 {body.guests}")
|
|
if body.message:
|
|
lines += ["", "요청사항", body.message.strip()]
|
|
lines += [
|
|
"",
|
|
"— 이 메일은 발행 사이트의 예약 요청 폼에서 자동으로 보냈습니다.",
|
|
" 예약이 확정된 것은 아니며, 손님에게 직접 연락하셔야 합니다.",
|
|
]
|
|
return "\n".join(lines)
|