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:
AutonetSellCar Deploy
2025-12-31 18:10:27 +09:00
parent be7f12bbf3
commit 2a8e32f427
8 changed files with 230 additions and 73 deletions

View File

@@ -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 | **딜러 설명 번역 시스템 추가**: Azure Translator API 연동, 한국어→영어/몽골어/러시아어 직접 번역 |
| 2024-12-27 | 관리자 번역 관리 페이지 추가 (`/admin/dealer-translations`) | | 2024-12-27 | 관리자 번역 관리 페이지 추가 (`/admin/dealer-translations`) |
| 2024-12-27 | DB 스키마 확장: `dealer_description_en/mn/ru` 컬럼 추가 | | 2024-12-27 | DB 스키마 확장: `dealer_description_en/mn/ru` 컬럼 추가 |

View File

@@ -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) @router.get("/admin/{banner_id}", response_model=HeroBannerResponse)
def admin_get_banner( def admin_get_banner(
banner_id: int, banner_id: int,
@@ -218,27 +258,6 @@ def delete_banner(
return {"message": "Banner deleted successfully"} 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 ==================== # ==================== Image Upload ====================
@router.post("/admin/upload-image") @router.post("/admin/upload-image")
@@ -297,18 +316,19 @@ def toggle_banner(
): ):
"""차량의 배너 상태 토글 (Admin) """차량의 배너 상태 토글 (Admin)
- is_banner=False → True: HeroBanner 생성 - HeroBanner 존재 → 삭제
- is_banner=True → False: HeroBanner 삭제 - HeroBanner 없음 → 생성
""" """
car = db.query(Car).filter(Car.id == car_id).first() car = db.query(Car).filter(Car.id == car_id).first()
if not car: if not car:
raise HTTPException(status_code=404, detail="Car not found") 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() db.delete(existing_banner)
if banner:
db.delete(banner)
car.is_banner = False car.is_banner = False
db.commit() db.commit()
return {"car_id": car_id, "is_banner": False, "message": "Removed from banner"} return {"car_id": car_id, "is_banner": False, "message": "Removed from banner"}
@@ -341,22 +361,3 @@ def toggle_banner(
db.commit() db.commit()
db.refresh(banner) db.refresh(banner)
return {"car_id": car_id, "is_banner": True, "banner_id": banner.id, "message": "Added to 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)}

View File

@@ -29,6 +29,12 @@ class SystemSettings(Base):
container_logistics_usd = Column(Integer, default=3600) # 컨테이너 물류비 $3,600 container_logistics_usd = Column(Integer, default=3600) # 컨테이너 물류비 $3,600
shoring_cost_usd = Column(Integer, default=300) # 쇼링비 (컨테이너 고정) $300 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_enabled = Column(Boolean, default=True) # 레퍼럴 보상 활성화
referral_reward_percent = Column(Float, default=10.0) # 보상 비율 (기본 10%) referral_reward_percent = Column(Float, default=10.0) # 보상 비율 (기본 10%)

View File

@@ -15,6 +15,11 @@ class SystemSettingsUpdate(BaseModel):
cache_ttl_hours: Optional[int] = None cache_ttl_hours: Optional[int] = None
container_logistics_usd: Optional[int] = None container_logistics_usd: Optional[int] = None
shoring_cost_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): class SystemSettingsResponse(BaseModel):
@@ -30,6 +35,11 @@ class SystemSettingsResponse(BaseModel):
cache_ttl_hours: int cache_ttl_hours: int
container_logistics_usd: int container_logistics_usd: int
shoring_cost_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 created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None updated_at: Optional[datetime] = None

View File

@@ -14,6 +14,8 @@ interface SystemSettings {
cache_ttl_hours: number; cache_ttl_hours: number;
container_logistics_usd: number; container_logistics_usd: number;
shoring_cost_usd: number; shoring_cost_usd: number;
domestic_export_customs_krw: number;
show_dealer_comment: boolean;
referral_reward_enabled: boolean; referral_reward_enabled: boolean;
referral_reward_percent: number; referral_reward_percent: number;
referral_reward_type: string; referral_reward_type: string;
@@ -57,6 +59,8 @@ export default function SettingsPage() {
cache_ttl_hours: 2, cache_ttl_hours: 2,
container_logistics_usd: 3600, container_logistics_usd: 3600,
shoring_cost_usd: 300, shoring_cost_usd: 300,
domestic_export_customs_krw: 1150000,
show_dealer_comment: true,
referral_reward_enabled: true, referral_reward_enabled: true,
referral_reward_percent: 10.0, referral_reward_percent: 10.0,
referral_reward_type: 'one_time', referral_reward_type: 'one_time',
@@ -84,6 +88,8 @@ export default function SettingsPage() {
cache_ttl_hours: data.cache_ttl_hours, cache_ttl_hours: data.cache_ttl_hours,
container_logistics_usd: data.container_logistics_usd || 3600, container_logistics_usd: data.container_logistics_usd || 3600,
shoring_cost_usd: data.shoring_cost_usd || 300, 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_enabled: data.referral_reward_enabled ?? true,
referral_reward_percent: data.referral_reward_percent ?? 10.0, referral_reward_percent: data.referral_reward_percent ?? 10.0,
referral_reward_type: data.referral_reward_type || 'one_time', referral_reward_type: data.referral_reward_type || 'one_time',
@@ -234,6 +240,31 @@ export default function SettingsPage() {
</div> </div>
</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 */} {/* Margin Settings */}
<div className="bg-white rounded-xl shadow-sm p-6"> <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"> <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> <p className="mt-1 text-sm text-gray-500"> - (기본값: $300)</p>
</div> </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>
<div className="mt-4 p-4 bg-blue-50 rounded-lg"> <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>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>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>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> </div>
</div> </div>

View File

@@ -3,22 +3,39 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { visitorApi, VisitorStatsOverview, TopPage, TopReferrer, ChartData, RealtimeStats } from '@/lib/api'; import { visitorApi, VisitorStatsOverview, TopPage, TopReferrer, ChartData, RealtimeStats } from '@/lib/api';
// Country code to name mapping // Country code to name and flag mapping
const countryNames: Record<string, string> = { const countryInfo: Record<string, { name: string; flag: string }> = {
MN: 'Mongolia', MN: { name: 'Mongolia', flag: '🇲🇳' },
RU: 'Russia', RU: { name: 'Russia', flag: '🇷🇺' },
KR: 'Korea', KR: { name: 'Korea', flag: '🇰🇷' },
US: 'United States', US: { name: 'United States', flag: '🇺🇸' },
CN: 'China', CN: { name: 'China', flag: '🇨🇳' },
JP: 'Japan', JP: { name: 'Japan', flag: '🇯🇵' },
DE: 'Germany', DE: { name: 'Germany', flag: '🇩🇪' },
FR: 'France', FR: { name: 'France', flag: '🇫🇷' },
GB: 'United Kingdom', GB: { name: 'United Kingdom', flag: '🇬🇧' },
AU: 'Australia', AU: { name: 'Australia', flag: '🇦🇺' },
unknown: 'Unknown', CA: { name: 'Canada', flag: '🇨🇦' },
LO: 'Local', 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 // Simple bar chart component
const SimpleBarChart = ({ data, color = 'bg-primary-500', height = 120 }: { const SimpleBarChart = ({ data, color = 'bg-primary-500', height = 120 }: {
data: ChartData | null; 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() { export default function VisitorStatsPage() {
const [overview, setOverview] = useState<VisitorStatsOverview | null>(null); const [overview, setOverview] = useState<VisitorStatsOverview | null>(null);
const [visitsChart, setVisitsChart] = useState<ChartData | null>(null); const [visitsChart, setVisitsChart] = useState<ChartData | null>(null);
@@ -315,8 +387,11 @@ export default function VisitorStatsPage() {
</div> </div>
</div> </div>
{/* Breakdowns */} {/* Country Stats - Full Width */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <CountryStatsCard data={overview?.country_breakdown || {}} />
{/* Device & Browser Breakdowns */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<BreakdownCard <BreakdownCard
title="By Device" title="By Device"
data={overview?.device_breakdown || {}} data={overview?.device_breakdown || {}}
@@ -327,12 +402,6 @@ export default function VisitorStatsPage() {
data={overview?.browser_breakdown || {}} data={overview?.browser_breakdown || {}}
icon="&#127760;" icon="&#127760;"
/> />
<BreakdownCard
title="By Country"
data={overview?.country_breakdown || {}}
icon="&#127757;"
nameMap={countryNames}
/>
</div> </div>
{/* Tables */} {/* Tables */}

View File

@@ -51,10 +51,26 @@ export default function CarDetailPage() {
// Collapsible sections // Collapsible sections
const [showDealerComment, setShowDealerComment] = useState(false); const [showDealerComment, setShowDealerComment] = useState(false);
// System settings
const [showDealerCommentSetting, setShowDealerCommentSetting] = useState(true);
useEffect(() => { useEffect(() => {
if (params.id) { if (params.id) {
loadCar(Number(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]); }, [params.id]);
useEffect(() => { useEffect(() => {
@@ -458,7 +474,7 @@ export default function CarDetailPage() {
)} )}
{/* Dealer's Comment - Left Column (Collapsible) - Requires 0.1 CC */} {/* 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"> <div className="mt-4 bg-amber-50 border border-amber-200 rounded-lg overflow-hidden">
<button <button
onClick={() => setShowDealerComment(!showDealerComment)} onClick={() => setShowDealerComment(!showDealerComment)}
@@ -921,7 +937,7 @@ export default function CarDetailPage() {
)} )}
{/* Unlock section for banner cars without performance check access */} {/* 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"> <div className="bg-white rounded-lg shadow p-6 mb-6">
<h2 className="text-lg font-semibold mb-4 flex items-center"> <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"> <svg className="w-5 h-5 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View File

@@ -9,7 +9,6 @@ import SidebarLayout from '@/components/SidebarLayout';
import { useAuthStore } from '@/lib/store'; import { useAuthStore } from '@/lib/store';
// Cost constants // Cost constants
const DOMESTIC_COST_KRW = 1150000; // ₩1,150,000
const KOREAN_FEE_PERCENT = 5; // 5% of vehicle price const KOREAN_FEE_PERCENT = 5; // 5% of vehicle price
const MONGOLIAN_FEE_PERCENT = 5; // 5% of vehicle price const MONGOLIAN_FEE_PERCENT = 5; // 5% of vehicle price
const CUSTOMS_FEE_USD = 200; // $200 const CUSTOMS_FEE_USD = 200; // $200
@@ -35,6 +34,7 @@ interface ContainerSlot {
interface Settings { interface Settings {
container_logistics_usd: number; container_logistics_usd: number;
shoring_cost_usd: number; shoring_cost_usd: number;
domestic_export_customs_krw: number;
} }
export default function CostPage() { export default function CostPage() {
@@ -91,6 +91,7 @@ export default function CostPage() {
const [settings, setSettings] = useState<Settings>({ const [settings, setSettings] = useState<Settings>({
container_logistics_usd: 3600, container_logistics_usd: 3600,
shoring_cost_usd: 300, shoring_cost_usd: 300,
domestic_export_customs_krw: 1150000,
}); });
// Calculator state // Calculator state
@@ -122,6 +123,7 @@ export default function CostPage() {
setSettings({ setSettings({
container_logistics_usd: data.container_logistics_usd || 3600, container_logistics_usd: data.container_logistics_usd || 3600,
shoring_cost_usd: data.shoring_cost_usd || 300, shoring_cost_usd: data.shoring_cost_usd || 300,
domestic_export_customs_krw: data.domestic_export_customs_krw || 1150000,
}); });
} }
} catch (error) { } catch (error) {
@@ -215,7 +217,7 @@ export default function CostPage() {
const usdToKrw = useExchangeRateStore.getState().rates.USD?.rate || 1483; const usdToKrw = useExchangeRateStore.getState().rates.USD?.rate || 1483;
// Korean domestic cost // Korean domestic cost
const domesticCost = DOMESTIC_COST_KRW; const domesticCost = settings.domestic_export_customs_krw;
// Korean fee (5% of vehicle price) // Korean fee (5% of vehicle price)
const koreanFee = priceKrw * (KOREAN_FEE_PERCENT / 100); const koreanFee = priceKrw * (KOREAN_FEE_PERCENT / 100);
@@ -346,7 +348,7 @@ export default function CostPage() {
</div> </div>
<div className="flex justify-between items-center py-2 border-b"> <div className="flex justify-between items-center py-2 border-b">
<span className="text-gray-600">{t.koreanDomesticCost}</span> <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>
<div className="flex justify-between items-center py-2 border-b"> <div className="flex justify-between items-center py-2 border-b">
<span className="text-gray-600">{t.koreanMargin}</span> <span className="text-gray-600">{t.koreanMargin}</span>