fix(enhance-v4): APK QR 버그 수정 + 웹메일 라우터 수정

This commit is contained in:
DESKTOP-TKLFCPR\ython 2026-06-02 20:21:07 +09:00
parent 1057915729
commit f00388b066
3 changed files with 16 additions and 20 deletions

View File

@ -5457,7 +5457,8 @@ class AppVersion(Base):
android_url = Column(String(1000), nullable=True) android_url = Column(String(1000), nullable=True)
ios_url = Column(String(1000), nullable=True) ios_url = Column(String(1000), nullable=True)
qr_image_path = Column(String(500), nullable=True) qr_image_path = Column(String(500), nullable=True)
landing_token = Column(String(36), nullable=True, unique=True) qr_data = Column(Text, nullable=True) # base64 PNG
landing_token = Column(String(64), nullable=True, unique=True)
download_count = Column(Integer, default=0) download_count = Column(Integer, default=0)
is_latest = Column(Boolean, default=False) is_latest = Column(Boolean, default=False)
release_notes = Column(Text, nullable=True) release_notes = Column(Text, nullable=True)

View File

@ -97,9 +97,10 @@ async def upload_apk(
file_path.write_bytes(file_bytes) file_path.write_bytes(file_bytes)
# 기존 latest 해제 # 기존 latest 해제
from sqlalchemy import update as sa_update
await db.execute( await db.execute(
__import__('sqlalchemy', fromlist=['update']).update(AppVersion) sa_update(AppVersion)
.where(AppVersion.tenant_id == user.tenant_id, AppVersion.is_latest == True) .where(AppVersion.is_latest == True)
.values(is_latest=False) .values(is_latest=False)
) )
@ -110,11 +111,10 @@ async def upload_apk(
qr_b64 = base64.b64encode(qr_bytes).decode() qr_b64 = base64.b64encode(qr_bytes).decode()
app_ver = AppVersion( app_ver = AppVersion(
tenant_id=user.tenant_id,
version=version, version=version,
platform="ANDROID", platform="ANDROID",
file_path=str(file_path), file_path=str(file_path),
file_size=len(file_bytes), file_size_mb=round(len(file_bytes) / 1024 / 1024, 2),
android_url=f"{BASE_URL}/api/app/download?token={token}", android_url=f"{BASE_URL}/api/app/download?token={token}",
ios_url=ios_url or None, ios_url=ios_url or None,
landing_token=token, landing_token=token,
@ -147,9 +147,10 @@ async def set_app_url(
user: User = Depends(require_admin_role), user: User = Depends(require_admin_role),
): ):
"""외부 URL(EAS 빌드 등)로 QR 코드 생성.""" """외부 URL(EAS 빌드 등)로 QR 코드 생성."""
from sqlalchemy import update as sa_update
await db.execute( await db.execute(
__import__('sqlalchemy', fromlist=['update']).update(AppVersion) sa_update(AppVersion)
.where(AppVersion.tenant_id == user.tenant_id, AppVersion.is_latest == True) .where(AppVersion.is_latest == True)
.values(is_latest=False) .values(is_latest=False)
) )
@ -158,7 +159,6 @@ async def set_app_url(
qr_bytes = _generate_qr(landing_url) qr_bytes = _generate_qr(landing_url)
app_ver = AppVersion( app_ver = AppVersion(
tenant_id=user.tenant_id,
version=req.version, version=req.version,
platform="BOTH" if req.ios_url else "ANDROID", platform="BOTH" if req.ios_url else "ANDROID",
android_url=req.android_url, android_url=req.android_url,
@ -190,10 +190,7 @@ async def get_latest(
): ):
"""최신 버전 정보 조회.""" """최신 버전 정보 조회."""
row = await db.execute( row = await db.execute(
select(AppVersion).where( select(AppVersion).where(AppVersion.is_latest == True)
AppVersion.tenant_id == user.tenant_id,
AppVersion.is_latest == True,
)
) )
ver = row.scalar_one_or_none() ver = row.scalar_one_or_none()
if not ver: if not ver:
@ -303,8 +300,7 @@ async def app_landing(
version_id=ver.id, version_id=ver.id,
platform="IOS" if is_ios else "ANDROID", platform="IOS" if is_ios else "ANDROID",
user_agent=request.headers.get("User-Agent", "")[:200], user_agent=request.headers.get("User-Agent", "")[:200],
ip_addr=request.client.host if request.client else "", downloaded_at=datetime.utcnow(),
accessed_at=datetime.utcnow(),
) )
db.add(log) db.add(log)
await db.commit() await db.commit()
@ -343,8 +339,7 @@ async def list_versions(
user: User = Depends(get_current_user), user: User = Depends(get_current_user),
): ):
rows = await db.execute( rows = await db.execute(
select(AppVersion).where(AppVersion.tenant_id == user.tenant_id) select(AppVersion).order_by(desc(AppVersion.created_at)).limit(20)
.order_by(desc(AppVersion.created_at)).limit(20)
) )
versions = rows.scalars().all() versions = rows.scalars().all()
return [ return [
@ -353,7 +348,7 @@ async def list_versions(
"download_count": v.download_count, "is_latest": v.is_latest, "download_count": v.download_count, "is_latest": v.is_latest,
"qr_url": f"{BASE_URL}/api/app/qr?token={v.landing_token}", "qr_url": f"{BASE_URL}/api/app/qr?token={v.landing_token}",
"landing_url": f"{BASE_URL}/api/app/landing?token={v.landing_token}", "landing_url": f"{BASE_URL}/api/app/landing?token={v.landing_token}",
"file_size_mb": round((v.file_size or 0) / 1024 / 1024, 1), "file_size_mb": round((v.file_size_mb or 0), 1),
"release_notes": v.release_notes, "release_notes": v.release_notes,
"created_at": v.created_at, "created_at": v.created_at,
} }
@ -368,7 +363,7 @@ async def delete_version(
user: User = Depends(require_admin_role), user: User = Depends(require_admin_role),
): ):
row = await db.execute( row = await db.execute(
select(AppVersion).where(AppVersion.id == version_id, AppVersion.tenant_id == user.tenant_id) select(AppVersion).where(AppVersion.id == version_id)
) )
ver = row.scalar_one_or_none() ver = row.scalar_one_or_none()
if not ver: if not ver:
@ -388,7 +383,7 @@ async def app_stats(
user: User = Depends(get_current_user), user: User = Depends(get_current_user),
): ):
total = (await db.execute( total = (await db.execute(
select(func.sum(AppVersion.download_count)).where(AppVersion.tenant_id == user.tenant_id) select(func.sum(AppVersion.download_count))
)).scalar() or 0 )).scalar() or 0
android = (await db.execute( android = (await db.execute(
select(func.count(AppDownloadLog.id)).where(AppDownloadLog.platform == "ANDROID") select(func.count(AppDownloadLog.id)).where(AppDownloadLog.platform == "ANDROID")

View File

@ -42,7 +42,7 @@ class NotifyRuleCreate(BaseModel):
name: str = Field(..., max_length=200) name: str = Field(..., max_length=200)
trigger_type: str = Field(..., description="SR_CREATED|SR_UPDATED|INCIDENT|DRIFT|KPI_BREACH|CUSTOM") trigger_type: str = Field(..., description="SR_CREATED|SR_UPDATED|INCIDENT|DRIFT|KPI_BREACH|CUSTOM")
conditions: dict = Field(default_factory=dict) conditions: dict = Field(default_factory=dict)
channels: List[str] = Field(default_factory=list, description=["messenger","email","slack","kakao"]) channels: List[str] = Field(default_factory=list, description="messenger|email|slack|kakao")
priority_filter: str = Field("ALL", description="HIGH|MEDIUM|ALL") priority_filter: str = Field("ALL", description="HIGH|MEDIUM|ALL")
silence_hours: Optional[List[int]] = Field(None, description="무음 시간 목록 [22,23,0,1,...,7]") silence_hours: Optional[List[int]] = Field(None, description="무음 시간 목록 [22,23,0,1,...,7]")
digest_mode: bool = Field(False, description="묶음 발송 모드") digest_mode: bool = Field(False, description="묶음 발송 모드")