feat: Add admin settings for dealer comment & domestic cost, enhance visitor country stats
- Add show_dealer_comment toggle to admin settings - Add domestic_export_customs_krw setting for cost page - Cost page now uses dynamic settings instead of hardcoded values - Enhance Visitor Stats with dedicated Country Stats card with flags - Fix hero_banners API route ordering (422 error fix) - Fix banner toggle logic to check HeroBanner table instead of car.is_banner - Add country flag emojis for 23+ countries 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -660,6 +660,11 @@ Import 시 자동 번역 (Azure API)
|
||||
|
||||
| 날짜 | 변경 내용 |
|
||||
|------|----------|
|
||||
| 2024-12-31 | **Admin Settings 기능 추가**: Show Dealer Comment 토글, Korean Domestic + Export Customs 금액 설정 |
|
||||
| 2024-12-31 | Cost 페이지에서 국내비용+수출통관비용 동적 적용 (settings API 연동) |
|
||||
| 2024-12-31 | **Visitor Stats 국가별 통계 강화**: 국기 이모지 추가, 전용 Country Stats 카드 추가 |
|
||||
| 2024-12-31 | Hero Banners API 라우트 순서 수정 (422 에러 해결) |
|
||||
| 2024-12-31 | Banner Toggle 로직 수정 (HeroBanner 테이블 기준으로 변경) |
|
||||
| 2024-12-27 | **딜러 설명 번역 시스템 추가**: Azure Translator API 연동, 한국어→영어/몽골어/러시아어 직접 번역 |
|
||||
| 2024-12-27 | 관리자 번역 관리 페이지 추가 (`/admin/dealer-translations`) |
|
||||
| 2024-12-27 | DB 스키마 확장: `dealer_description_en/mn/ru` 컬럼 추가 |
|
||||
|
||||
@@ -143,6 +143,46 @@ def get_banner_cars(
|
||||
}
|
||||
|
||||
|
||||
@router.put("/admin/reorder")
|
||||
def reorder_banners(
|
||||
request: BannerReorderRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""배너 순서 재정렬 (Admin)
|
||||
|
||||
car_ids: 배너 차량 ID 목록 (원하는 순서대로)
|
||||
"""
|
||||
for order, car_id in enumerate(request.car_ids):
|
||||
banner = db.query(HeroBanner).filter(HeroBanner.car_id == car_id).first()
|
||||
if banner:
|
||||
banner.display_order = order
|
||||
|
||||
db.commit()
|
||||
return {"message": "Banner order updated", "count": len(request.car_ids)}
|
||||
|
||||
|
||||
@router.put("/admin/settings", response_model=HeroBannerSettingsResponse)
|
||||
def update_banner_settings(
|
||||
settings_data: HeroBannerSettingsUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""배너 슬라이더 설정 수정 (Admin)"""
|
||||
settings_obj = db.query(HeroBannerSettings).first()
|
||||
if not settings_obj:
|
||||
settings_obj = HeroBannerSettings()
|
||||
db.add(settings_obj)
|
||||
|
||||
update_data = settings_data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(settings_obj, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(settings_obj)
|
||||
return settings_obj
|
||||
|
||||
|
||||
@router.get("/admin/{banner_id}", response_model=HeroBannerResponse)
|
||||
def admin_get_banner(
|
||||
banner_id: int,
|
||||
@@ -218,27 +258,6 @@ def delete_banner(
|
||||
return {"message": "Banner deleted successfully"}
|
||||
|
||||
|
||||
@router.put("/admin/settings", response_model=HeroBannerSettingsResponse)
|
||||
def update_banner_settings(
|
||||
settings_data: HeroBannerSettingsUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""배너 슬라이더 설정 수정 (Admin)"""
|
||||
settings_obj = db.query(HeroBannerSettings).first()
|
||||
if not settings_obj:
|
||||
settings_obj = HeroBannerSettings()
|
||||
db.add(settings_obj)
|
||||
|
||||
update_data = settings_data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(settings_obj, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(settings_obj)
|
||||
return settings_obj
|
||||
|
||||
|
||||
# ==================== Image Upload ====================
|
||||
|
||||
@router.post("/admin/upload-image")
|
||||
@@ -297,18 +316,19 @@ def toggle_banner(
|
||||
):
|
||||
"""차량의 배너 상태 토글 (Admin)
|
||||
|
||||
- is_banner=False → True: HeroBanner 생성
|
||||
- is_banner=True → False: HeroBanner 삭제
|
||||
- HeroBanner 존재 → 삭제
|
||||
- HeroBanner 없음 → 생성
|
||||
"""
|
||||
car = db.query(Car).filter(Car.id == car_id).first()
|
||||
if not car:
|
||||
raise HTTPException(status_code=404, detail="Car not found")
|
||||
|
||||
if car.is_banner:
|
||||
# HeroBanner 테이블을 기준으로 판단 (car.is_banner 필드 대신)
|
||||
existing_banner = db.query(HeroBanner).filter(HeroBanner.car_id == car_id).first()
|
||||
|
||||
if existing_banner:
|
||||
# 배너에서 제거
|
||||
banner = db.query(HeroBanner).filter(HeroBanner.car_id == car_id).first()
|
||||
if banner:
|
||||
db.delete(banner)
|
||||
db.delete(existing_banner)
|
||||
car.is_banner = False
|
||||
db.commit()
|
||||
return {"car_id": car_id, "is_banner": False, "message": "Removed from banner"}
|
||||
@@ -341,22 +361,3 @@ def toggle_banner(
|
||||
db.commit()
|
||||
db.refresh(banner)
|
||||
return {"car_id": car_id, "is_banner": True, "banner_id": banner.id, "message": "Added to banner"}
|
||||
|
||||
|
||||
@router.put("/admin/reorder")
|
||||
def reorder_banners(
|
||||
request: BannerReorderRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""배너 순서 재정렬 (Admin)
|
||||
|
||||
car_ids: 배너 차량 ID 목록 (원하는 순서대로)
|
||||
"""
|
||||
for order, car_id in enumerate(request.car_ids):
|
||||
banner = db.query(HeroBanner).filter(HeroBanner.car_id == car_id).first()
|
||||
if banner:
|
||||
banner.display_order = order
|
||||
|
||||
db.commit()
|
||||
return {"message": "Banner order updated", "count": len(request.car_ids)}
|
||||
|
||||
@@ -29,6 +29,12 @@ class SystemSettings(Base):
|
||||
container_logistics_usd = Column(Integer, default=3600) # 컨테이너 물류비 $3,600
|
||||
shoring_cost_usd = Column(Integer, default=300) # 쇼링비 (컨테이너 고정) $300
|
||||
|
||||
# 국내비용 + 수출통관비용 (KRW)
|
||||
domestic_export_customs_krw = Column(Integer, default=1150000) # ₩1,150,000
|
||||
|
||||
# 딜러 코멘트 표시 설정
|
||||
show_dealer_comment = Column(Boolean, default=True) # 딜러 코멘트 표시 여부
|
||||
|
||||
# 레퍼럴 보상 설정
|
||||
referral_reward_enabled = Column(Boolean, default=True) # 레퍼럴 보상 활성화
|
||||
referral_reward_percent = Column(Float, default=10.0) # 보상 비율 (기본 10%)
|
||||
|
||||
@@ -15,6 +15,11 @@ class SystemSettingsUpdate(BaseModel):
|
||||
cache_ttl_hours: Optional[int] = None
|
||||
container_logistics_usd: Optional[int] = None
|
||||
shoring_cost_usd: Optional[int] = None
|
||||
domestic_export_customs_krw: Optional[int] = None
|
||||
show_dealer_comment: Optional[bool] = None
|
||||
referral_reward_enabled: Optional[bool] = None
|
||||
referral_reward_percent: Optional[float] = None
|
||||
referral_reward_type: Optional[str] = None
|
||||
|
||||
|
||||
class SystemSettingsResponse(BaseModel):
|
||||
@@ -30,6 +35,11 @@ class SystemSettingsResponse(BaseModel):
|
||||
cache_ttl_hours: int
|
||||
container_logistics_usd: int
|
||||
shoring_cost_usd: int
|
||||
domestic_export_customs_krw: int = 1150000
|
||||
show_dealer_comment: bool = True
|
||||
referral_reward_enabled: bool = True
|
||||
referral_reward_percent: float = 10.0
|
||||
referral_reward_type: str = "one_time"
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ interface SystemSettings {
|
||||
cache_ttl_hours: number;
|
||||
container_logistics_usd: number;
|
||||
shoring_cost_usd: number;
|
||||
domestic_export_customs_krw: number;
|
||||
show_dealer_comment: boolean;
|
||||
referral_reward_enabled: boolean;
|
||||
referral_reward_percent: number;
|
||||
referral_reward_type: string;
|
||||
@@ -57,6 +59,8 @@ export default function SettingsPage() {
|
||||
cache_ttl_hours: 2,
|
||||
container_logistics_usd: 3600,
|
||||
shoring_cost_usd: 300,
|
||||
domestic_export_customs_krw: 1150000,
|
||||
show_dealer_comment: true,
|
||||
referral_reward_enabled: true,
|
||||
referral_reward_percent: 10.0,
|
||||
referral_reward_type: 'one_time',
|
||||
@@ -84,6 +88,8 @@ export default function SettingsPage() {
|
||||
cache_ttl_hours: data.cache_ttl_hours,
|
||||
container_logistics_usd: data.container_logistics_usd || 3600,
|
||||
shoring_cost_usd: data.shoring_cost_usd || 300,
|
||||
domestic_export_customs_krw: data.domestic_export_customs_krw || 1150000,
|
||||
show_dealer_comment: data.show_dealer_comment ?? true,
|
||||
referral_reward_enabled: data.referral_reward_enabled ?? true,
|
||||
referral_reward_percent: data.referral_reward_percent ?? 10.0,
|
||||
referral_reward_type: data.referral_reward_type || 'one_time',
|
||||
@@ -234,6 +240,31 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Display Settings */}
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-4 flex items-center gap-2">
|
||||
<span>Display Settings</span>
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.show_dealer_comment}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, show_dealer_comment: e.target.checked }))}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-600"></div>
|
||||
</label>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700">Show Dealer Comment</span>
|
||||
<p className="text-sm text-gray-500">차량 상세 페이지에서 딜러 코멘트 표시 여부</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Margin Settings */}
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-4 flex items-center gap-2">
|
||||
@@ -388,6 +419,22 @@ export default function SettingsPage() {
|
||||
/>
|
||||
<p className="mt-1 text-sm text-gray-500">쇼링비 - 컨테이너 고정 비용 (기본값: $300)</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Korean Domestic + Export Customs (KRW)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="10000000"
|
||||
step="10000"
|
||||
value={formData.domestic_export_customs_krw}
|
||||
onChange={(e) => handleChange('domestic_export_customs_krw', parseInt(e.target.value) || 1150000)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
/>
|
||||
<p className="mt-1 text-sm text-gray-500">국내비용 + 수출통관비용 (기본값: ₩1,150,000)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
|
||||
@@ -396,6 +443,7 @@ export default function SettingsPage() {
|
||||
<p>Total Container Cost: ${formData.container_logistics_usd + formData.shoring_cost_usd}</p>
|
||||
<p>Small Car (5.5/10): ${((formData.container_logistics_usd + formData.shoring_cost_usd) * 0.275).toFixed(0)} per car</p>
|
||||
<p>Compact Car (4.5/10): ${((formData.container_logistics_usd + formData.shoring_cost_usd) * 0.225).toFixed(0)} per car</p>
|
||||
<p className="mt-2 pt-2 border-t border-blue-200">Korean Domestic + Export Customs: ₩{formData.domestic_export_customs_krw.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,22 +3,39 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { visitorApi, VisitorStatsOverview, TopPage, TopReferrer, ChartData, RealtimeStats } from '@/lib/api';
|
||||
|
||||
// Country code to name mapping
|
||||
const countryNames: Record<string, string> = {
|
||||
MN: 'Mongolia',
|
||||
RU: 'Russia',
|
||||
KR: 'Korea',
|
||||
US: 'United States',
|
||||
CN: 'China',
|
||||
JP: 'Japan',
|
||||
DE: 'Germany',
|
||||
FR: 'France',
|
||||
GB: 'United Kingdom',
|
||||
AU: 'Australia',
|
||||
unknown: 'Unknown',
|
||||
LO: 'Local',
|
||||
// Country code to name and flag mapping
|
||||
const countryInfo: Record<string, { name: string; flag: string }> = {
|
||||
MN: { name: 'Mongolia', flag: '🇲🇳' },
|
||||
RU: { name: 'Russia', flag: '🇷🇺' },
|
||||
KR: { name: 'Korea', flag: '🇰🇷' },
|
||||
US: { name: 'United States', flag: '🇺🇸' },
|
||||
CN: { name: 'China', flag: '🇨🇳' },
|
||||
JP: { name: 'Japan', flag: '🇯🇵' },
|
||||
DE: { name: 'Germany', flag: '🇩🇪' },
|
||||
FR: { name: 'France', flag: '🇫🇷' },
|
||||
GB: { name: 'United Kingdom', flag: '🇬🇧' },
|
||||
AU: { name: 'Australia', flag: '🇦🇺' },
|
||||
CA: { name: 'Canada', flag: '🇨🇦' },
|
||||
IN: { name: 'India', flag: '🇮🇳' },
|
||||
SG: { name: 'Singapore', flag: '🇸🇬' },
|
||||
HK: { name: 'Hong Kong', flag: '🇭🇰' },
|
||||
TW: { name: 'Taiwan', flag: '🇹🇼' },
|
||||
VN: { name: 'Vietnam', flag: '🇻🇳' },
|
||||
TH: { name: 'Thailand', flag: '🇹🇭' },
|
||||
MY: { name: 'Malaysia', flag: '🇲🇾' },
|
||||
ID: { name: 'Indonesia', flag: '🇮🇩' },
|
||||
PH: { name: 'Philippines', flag: '🇵🇭' },
|
||||
KZ: { name: 'Kazakhstan', flag: '🇰🇿' },
|
||||
UZ: { name: 'Uzbekistan', flag: '🇺🇿' },
|
||||
UA: { name: 'Ukraine', flag: '🇺🇦' },
|
||||
unknown: { name: 'Unknown', flag: '🌐' },
|
||||
LO: { name: 'Local', flag: '🏠' },
|
||||
};
|
||||
|
||||
const countryNames: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(countryInfo).map(([code, info]) => [code, info.name])
|
||||
);
|
||||
|
||||
// Simple bar chart component
|
||||
const SimpleBarChart = ({ data, color = 'bg-primary-500', height = 120 }: {
|
||||
data: ChartData | null;
|
||||
@@ -104,6 +121,61 @@ const BreakdownCard = ({ title, data, icon, nameMap }: {
|
||||
);
|
||||
};
|
||||
|
||||
// Country Stats Card with flags
|
||||
const CountryStatsCard = ({ data }: { data: Record<string, number> }) => {
|
||||
const total = Object.values(data).reduce((a, b) => a + b, 0);
|
||||
const sortedEntries = Object.entries(data).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
||||
|
||||
if (total === 0) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-2xl">🌍</span>
|
||||
<h3 className="text-lg font-semibold text-gray-800">Visitors by Country</h3>
|
||||
</div>
|
||||
<div className="text-gray-400 text-center py-8">No country data available</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">🌍</span>
|
||||
<h3 className="text-lg font-semibold text-gray-800">Visitors by Country</h3>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">Total: {total.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{sortedEntries.map(([code, value], index) => {
|
||||
const info = countryInfo[code] || { name: code, flag: '🌐' };
|
||||
const percentage = ((value / total) * 100).toFixed(1);
|
||||
|
||||
return (
|
||||
<div key={code} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<span className="text-2xl">{info.flag}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium text-gray-800 truncate">{info.name}</span>
|
||||
<span className="text-sm font-semibold text-primary-600">{value.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2 mt-1">
|
||||
<div
|
||||
className="h-2 rounded-full bg-gradient-to-r from-primary-400 to-primary-600"
|
||||
style={{ width: `${(value / sortedEntries[0][1]) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">{percentage}% of total</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function VisitorStatsPage() {
|
||||
const [overview, setOverview] = useState<VisitorStatsOverview | null>(null);
|
||||
const [visitsChart, setVisitsChart] = useState<ChartData | null>(null);
|
||||
@@ -315,8 +387,11 @@ export default function VisitorStatsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Breakdowns */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Country Stats - Full Width */}
|
||||
<CountryStatsCard data={overview?.country_breakdown || {}} />
|
||||
|
||||
{/* Device & Browser Breakdowns */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<BreakdownCard
|
||||
title="By Device"
|
||||
data={overview?.device_breakdown || {}}
|
||||
@@ -327,12 +402,6 @@ export default function VisitorStatsPage() {
|
||||
data={overview?.browser_breakdown || {}}
|
||||
icon="🌐"
|
||||
/>
|
||||
<BreakdownCard
|
||||
title="By Country"
|
||||
data={overview?.country_breakdown || {}}
|
||||
icon="🌍"
|
||||
nameMap={countryNames}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tables */}
|
||||
|
||||
@@ -51,10 +51,26 @@ export default function CarDetailPage() {
|
||||
// Collapsible sections
|
||||
const [showDealerComment, setShowDealerComment] = useState(false);
|
||||
|
||||
// System settings
|
||||
const [showDealerCommentSetting, setShowDealerCommentSetting] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (params.id) {
|
||||
loadCar(Number(params.id));
|
||||
}
|
||||
// Fetch system settings for dealer comment visibility
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/settings/`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setShowDealerCommentSetting(data.show_dealer_comment ?? true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings:', error);
|
||||
}
|
||||
};
|
||||
fetchSettings();
|
||||
}, [params.id]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -458,7 +474,7 @@ export default function CarDetailPage() {
|
||||
)}
|
||||
|
||||
{/* Dealer's Comment - Left Column (Collapsible) - Requires 0.1 CC */}
|
||||
{hasPerformanceCheckAccess && car.dealer_description && (
|
||||
{showDealerCommentSetting && hasPerformanceCheckAccess && car.dealer_description && (
|
||||
<div className="mt-4 bg-amber-50 border border-amber-200 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => setShowDealerComment(!showDealerComment)}
|
||||
@@ -921,7 +937,7 @@ export default function CarDetailPage() {
|
||||
)}
|
||||
|
||||
{/* Unlock section for banner cars without performance check access */}
|
||||
{!performanceCheck?.found && isBannerCar && !hasPerformanceCheckAccess && car.dealer_description && (
|
||||
{showDealerCommentSetting && !performanceCheck?.found && isBannerCar && !hasPerformanceCheckAccess && car.dealer_description && (
|
||||
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<svg className="w-5 h-5 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
@@ -9,7 +9,6 @@ import SidebarLayout from '@/components/SidebarLayout';
|
||||
import { useAuthStore } from '@/lib/store';
|
||||
|
||||
// Cost constants
|
||||
const DOMESTIC_COST_KRW = 1150000; // ₩1,150,000
|
||||
const KOREAN_FEE_PERCENT = 5; // 5% of vehicle price
|
||||
const MONGOLIAN_FEE_PERCENT = 5; // 5% of vehicle price
|
||||
const CUSTOMS_FEE_USD = 200; // $200
|
||||
@@ -35,6 +34,7 @@ interface ContainerSlot {
|
||||
interface Settings {
|
||||
container_logistics_usd: number;
|
||||
shoring_cost_usd: number;
|
||||
domestic_export_customs_krw: number;
|
||||
}
|
||||
|
||||
export default function CostPage() {
|
||||
@@ -91,6 +91,7 @@ export default function CostPage() {
|
||||
const [settings, setSettings] = useState<Settings>({
|
||||
container_logistics_usd: 3600,
|
||||
shoring_cost_usd: 300,
|
||||
domestic_export_customs_krw: 1150000,
|
||||
});
|
||||
|
||||
// Calculator state
|
||||
@@ -122,6 +123,7 @@ export default function CostPage() {
|
||||
setSettings({
|
||||
container_logistics_usd: data.container_logistics_usd || 3600,
|
||||
shoring_cost_usd: data.shoring_cost_usd || 300,
|
||||
domestic_export_customs_krw: data.domestic_export_customs_krw || 1150000,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -215,7 +217,7 @@ export default function CostPage() {
|
||||
const usdToKrw = useExchangeRateStore.getState().rates.USD?.rate || 1483;
|
||||
|
||||
// Korean domestic cost
|
||||
const domesticCost = DOMESTIC_COST_KRW;
|
||||
const domesticCost = settings.domestic_export_customs_krw;
|
||||
|
||||
// Korean fee (5% of vehicle price)
|
||||
const koreanFee = priceKrw * (KOREAN_FEE_PERCENT / 100);
|
||||
@@ -346,7 +348,7 @@ export default function CostPage() {
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b">
|
||||
<span className="text-gray-600">{t.koreanDomesticCost}</span>
|
||||
<span className="font-medium text-primary-600">{formatLocalCurrency(DOMESTIC_COST_KRW)}</span>
|
||||
<span className="font-medium text-primary-600">{formatLocalCurrency(settings.domestic_export_customs_krw)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b">
|
||||
<span className="text-gray-600">{t.koreanMargin}</span>
|
||||
|
||||
Reference in New Issue
Block a user