feat: 하이브로지스틱스 견적서 + 컨셉 시안 + 견적 UI 개선
- 하이브로지스틱스코리아 홈페이지 리뉴얼 견적서(docs) + 컨셉 시안(HTML) - 관리자 견적항목: grid→flex 레이아웃, 수량/선택 축소, 설명 확대 - 고객용 견적서: table-layout fixed, 카테고리 줄바꿈 방지, WBS 너비 통일 - PUT API wbs 필드 허용 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -388,7 +388,7 @@ export default function QuoteEditorPage() {
|
||||
|
||||
{/* ── 견적항목 ── */}
|
||||
{tab === '견적항목' && (
|
||||
<div className="max-w-5xl space-y-3">
|
||||
<div className="max-w-6xl space-y-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-slate-400 text-sm">선택 항목(optional)은 고객이 직접 선택/해제할 수 있습니다</p>
|
||||
<button onClick={addItem} className={addBtn}>+ 항목 추가</button>
|
||||
@@ -396,39 +396,39 @@ export default function QuoteEditorPage() {
|
||||
|
||||
{/* 헤더 */}
|
||||
{form.items.length > 0 && (
|
||||
<div className="grid grid-cols-12 gap-3 px-4 py-2 text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
||||
<div className="col-span-2">카테고리</div>
|
||||
<div className="col-span-3">항목명</div>
|
||||
<div className="col-span-3">설명</div>
|
||||
<div className="col-span-1 text-right">수량</div>
|
||||
<div className="col-span-1 text-right">단가</div>
|
||||
<div className="col-span-1 text-center">선택</div>
|
||||
<div className="col-span-1" />
|
||||
<div className="flex gap-3 px-4 py-2 text-xs font-semibold text-slate-500 uppercase tracking-wider">
|
||||
<div className="w-[100px] flex-shrink-0">카테고리</div>
|
||||
<div className="w-[200px] flex-shrink-0">항목명</div>
|
||||
<div className="flex-1 min-w-[200px]">설명</div>
|
||||
<div className="w-[60px] flex-shrink-0 text-right">수량</div>
|
||||
<div className="w-[120px] flex-shrink-0 text-right">단가</div>
|
||||
<div className="w-[50px] flex-shrink-0 text-center">선택</div>
|
||||
<div className="w-[32px] flex-shrink-0" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{form.items.length === 0 && <EmptyState icon="💰" msg="항목을 추가해 견적을 구성해보세요" />}
|
||||
|
||||
{form.items.map((item) => (
|
||||
<div key={item.id} className={`grid grid-cols-12 gap-3 px-4 py-3 rounded-xl border items-center transition-all ${item.optional ? 'bg-violet-900/10 border-violet-800/30' : 'bg-slate-900 border-slate-800'}`}>
|
||||
<div className="col-span-2">
|
||||
<div key={item.id} className={`flex gap-3 px-4 py-3 rounded-xl border items-center transition-all ${item.optional ? 'bg-violet-900/10 border-violet-800/30' : 'bg-slate-900 border-slate-800'}`}>
|
||||
<div className="w-[100px] flex-shrink-0">
|
||||
<select className={inpSm} value={item.category} onChange={(e) => updateItem(item.id, 'category', e.target.value)}>
|
||||
{ITEM_CATEGORIES.map((c) => <option key={c}>{c}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<div className="w-[200px] flex-shrink-0">
|
||||
<input className={inpSm} value={item.name} onChange={(e) => updateItem(item.id, 'name', e.target.value)} placeholder="항목명" />
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<input className={inpSm} value={item.description} onChange={(e) => updateItem(item.id, 'description', e.target.value)} placeholder="상세 설명" />
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<div className="w-[60px] flex-shrink-0">
|
||||
<input className={`${inpSm} text-right`} type="number" min={1} value={item.quantity} onChange={(e) => updateItem(item.id, 'quantity', Number(e.target.value))} />
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<div className="w-[120px] flex-shrink-0">
|
||||
<input className={`${inpSm} text-right`} type="number" min={0} step={10000} value={item.unitPrice} onChange={(e) => updateItem(item.id, 'unitPrice', Number(e.target.value))} />
|
||||
</div>
|
||||
<div className="col-span-1 flex justify-center">
|
||||
<div className="w-[50px] flex-shrink-0 flex justify-center">
|
||||
<button
|
||||
onClick={() => updateItem(item.id, 'optional', !item.optional)}
|
||||
title={item.optional ? '선택 항목 (클릭시 필수로)' : '필수 항목 (클릭시 선택으로)'}
|
||||
@@ -436,7 +436,7 @@ export default function QuoteEditorPage() {
|
||||
<span className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-all ${item.optional ? 'left-5' : 'left-0.5'}`} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="col-span-1 flex justify-end">
|
||||
<div className="w-[32px] flex-shrink-0 flex justify-end">
|
||||
<button onClick={() => removeItem(item.id)} className="text-slate-600 hover:text-red-400 transition-colors">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
||||
// ── 허용 필드 화이트리스트 (시스템 필드 변조 방지) ───────────
|
||||
const ALLOWED_FIELDS = [
|
||||
'title', 'client_name', 'client_email', 'client_phone',
|
||||
'items', 'maintenance', 'notes', 'status',
|
||||
'wbs', 'items', 'maintenance', 'notes', 'status',
|
||||
'valid_until', 'discount',
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -272,19 +272,24 @@ export default function QuotePage() {
|
||||
<h3 style={{ fontSize: 18, fontWeight: 700, color: 'white' }}>{phase.phase}</h3>
|
||||
</div>
|
||||
<div style={{ background: '#0f172a', borderRadius: 12, border: '1px solid rgba(255,255,255,0.06)', overflow: 'hidden' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed' }}>
|
||||
<colgroup>
|
||||
<col style={{ width: '28%' }} />
|
||||
<col style={{ width: '12%' }} />
|
||||
<col style={{ width: '60%' }} />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<th style={thStyle}>작업명</th>
|
||||
<th style={{ ...thStyle, width: 100 }}>기간</th>
|
||||
<th style={thStyle}>기간</th>
|
||||
<th style={thStyle}>설명</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{phase.tasks.map((task) => (
|
||||
<tr key={task.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
|
||||
<td style={tdStyle}>{task.name}</td>
|
||||
<td style={{ ...tdStyle, color: '#818cf8', fontWeight: 600 }}>{task.duration}</td>
|
||||
<td style={{ ...tdStyle, wordBreak: 'keep-all' }}>{task.name}</td>
|
||||
<td style={{ ...tdStyle, color: '#818cf8', fontWeight: 600, whiteSpace: 'nowrap' }}>{task.duration}</td>
|
||||
<td style={{ ...tdStyle, color: '#64748b' }}>{task.description || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -306,8 +311,16 @@ export default function QuotePage() {
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#60a5fa', display: 'inline-block' }} />
|
||||
필수 항목
|
||||
</h3>
|
||||
<div style={{ background: '#0f172a', borderRadius: 12, border: '1px solid rgba(255,255,255,0.06)', overflow: 'hidden' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<div style={{ background: '#0f172a', borderRadius: 12, border: '1px solid rgba(255,255,255,0.06)', overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed', minWidth: 700 }}>
|
||||
<colgroup>
|
||||
<col style={{ width: '10%' }} />
|
||||
<col style={{ width: '18%' }} />
|
||||
<col style={{ width: '42%' }} />
|
||||
<col style={{ width: '6%' }} />
|
||||
<col style={{ width: '12%' }} />
|
||||
<col style={{ width: '12%' }} />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<th style={thStyle}>카테고리</th>
|
||||
@@ -322,15 +335,15 @@ export default function QuotePage() {
|
||||
{requiredItems.map((item) => (
|
||||
<tr key={item.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
|
||||
<td style={tdStyle}>
|
||||
<span style={{ background: (CATEGORY_COLORS[item.category] || '#94a3b8') + '20', color: CATEGORY_COLORS[item.category] || '#94a3b8', fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 100 }}>
|
||||
<span style={{ background: (CATEGORY_COLORS[item.category] || '#94a3b8') + '20', color: CATEGORY_COLORS[item.category] || '#94a3b8', fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 100, whiteSpace: 'nowrap', display: 'inline-block' }}>
|
||||
{item.category}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ ...tdStyle, fontWeight: 600, color: 'white' }}>{item.name}</td>
|
||||
<td style={{ ...tdStyle, color: '#64748b' }}>{item.description || '—'}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', color: '#94a3b8' }}>{item.quantity}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', color: '#94a3b8', fontFamily: 'monospace' }}>{item.unitPrice.toLocaleString()}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', fontWeight: 700, color: 'white', fontFamily: 'monospace' }}>{(item.unitPrice * item.quantity).toLocaleString()}원</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', color: '#94a3b8', whiteSpace: 'nowrap' }}>{item.quantity}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', color: '#94a3b8', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>{item.unitPrice.toLocaleString()}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', fontWeight: 700, color: 'white', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>{(item.unitPrice * item.quantity).toLocaleString()}원</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -347,11 +360,19 @@ export default function QuotePage() {
|
||||
선택 항목
|
||||
</h3>
|
||||
<p style={{ color: '#475569', fontSize: 13, marginBottom: 12 }}>아래 항목 중 원하시는 것을 선택하세요 — 총 금액에 실시간으로 반영됩니다</p>
|
||||
<div style={{ background: '#0f172a', borderRadius: 12, border: '1px solid rgba(167,139,250,0.2)', overflow: 'hidden' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<div style={{ background: '#0f172a', borderRadius: 12, border: '1px solid rgba(167,139,250,0.2)', overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed', minWidth: 700 }}>
|
||||
<colgroup>
|
||||
<col style={{ width: '6%' }} />
|
||||
<col style={{ width: '10%' }} />
|
||||
<col style={{ width: '16%' }} />
|
||||
<col style={{ width: '42%' }} />
|
||||
<col style={{ width: '6%' }} />
|
||||
<col style={{ width: '12%' }} />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<th style={{ ...thStyle, width: 50 }}>선택</th>
|
||||
<th style={thStyle}>선택</th>
|
||||
<th style={thStyle}>카테고리</th>
|
||||
<th style={thStyle}>항목명</th>
|
||||
<th style={thStyle}>설명</th>
|
||||
@@ -368,14 +389,14 @@ export default function QuotePage() {
|
||||
<input type="checkbox" checked={!!checkedOptional[item.id]} onChange={() => {}} />
|
||||
</td>
|
||||
<td style={tdStyle}>
|
||||
<span style={{ background: (CATEGORY_COLORS[item.category] || '#94a3b8') + '20', color: CATEGORY_COLORS[item.category] || '#94a3b8', fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 100 }}>
|
||||
<span style={{ background: (CATEGORY_COLORS[item.category] || '#94a3b8') + '20', color: CATEGORY_COLORS[item.category] || '#94a3b8', fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 100, whiteSpace: 'nowrap', display: 'inline-block' }}>
|
||||
{item.category}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ ...tdStyle, fontWeight: 600, color: checkedOptional[item.id] ? 'white' : '#64748b' }}>{item.name}</td>
|
||||
<td style={{ ...tdStyle, color: '#475569' }}>{item.description || '—'}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', color: '#64748b' }}>{item.quantity}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', fontWeight: 700, color: checkedOptional[item.id] ? '#a78bfa' : '#475569', fontFamily: 'monospace' }}>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', color: '#64748b', whiteSpace: 'nowrap' }}>{item.quantity}</td>
|
||||
<td style={{ ...tdStyle, textAlign: 'right', fontWeight: 700, color: checkedOptional[item.id] ? '#a78bfa' : '#475569', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>
|
||||
{(item.unitPrice * item.quantity).toLocaleString()}원
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
117
docs/hive-logistics-quote.md
Normal file
117
docs/hive-logistics-quote.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# 하이브로지스틱스코리아 — 홈페이지 리뉴얼 견적서
|
||||
|
||||
## 기본 정보
|
||||
- 고객: 김정화 님 (주식회사 하이브로지스틱스코리아)
|
||||
- 플랫폼: 숨고
|
||||
- 발행일: 2026-04-11
|
||||
- 유효기간: 2026-04-25 (14일)
|
||||
|
||||
---
|
||||
|
||||
## 현재 사이트 분석
|
||||
|
||||
**URL**: https://hivelogistics-mo.imweb.me
|
||||
**플랫폼**: imweb (템플릿 기반)
|
||||
**페이지 구성** (6개 메뉴, 약 12개 해시 페이지):
|
||||
|
||||
| 메뉴 | 하위 | 비고 |
|
||||
|------|------|------|
|
||||
| 홈 | - | 메인 랜딩 |
|
||||
| 회사소개 | - | 기업 정보 |
|
||||
| 서비스 안내 | 해상운송, 항공운송, 국제특송, 내륙운송 | 4개 서브페이지 |
|
||||
| 파트너 네트워크 | - | 글로벌 거점 |
|
||||
| 고객지원 | 공지사항, FAQ, 고객센터 | 게시판 (현재 비어있음) |
|
||||
| 문의하기 | - | 문의 폼 + 파일첨부 |
|
||||
|
||||
**현재 문제점**:
|
||||
- imweb 템플릿 그대로 → 차별화 없음
|
||||
- 해시 기반 URL (/26u89wxb) → SEO 불리
|
||||
- 영문 버전 없음
|
||||
- 게시판 콘텐츠 비어있음
|
||||
|
||||
---
|
||||
|
||||
## 견적 구성
|
||||
|
||||
### 기본 패키지 — 250,000원
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| 디자인 리뉴얼 | 물류 전문 기업 느낌의 프리미엄 디자인 |
|
||||
| 반응형 | PC + 태블릿 + 모바일 완벽 대응 |
|
||||
| 페이지 구성 | 메인 / 회사소개 / 서비스(4종) / 파트너 / 고객지원 / 문의하기 |
|
||||
| SEO | 시맨틱 URL, 메타태그, OG 이미지 |
|
||||
| 문의 폼 | 이메일 자동 발송 |
|
||||
| 배포 | Vercel 호스팅 (무료) |
|
||||
| 소스코드 | 전체 이관 |
|
||||
|
||||
### 프리미엄 옵션 — 한/영 다국어 전환 +80,000원
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| 언어 토글 | 헤더에 KO / EN 전환 버튼 |
|
||||
| 전 페이지 적용 | 모든 페이지 한글↔영문 즉시 전환 |
|
||||
| 전제 | 영문 콘텐츠는 고객 제공 |
|
||||
|
||||
### 추가 옵션 (선택)
|
||||
|
||||
| 옵션 | 내용 | 금액 |
|
||||
|------|------|------|
|
||||
| A. 관리자 대시보드 | 문의 내역 조회, 공지사항 CRUD, 고객 목록 관리 | +150,000원 |
|
||||
| B. 공지사항 게시판 | 관리자가 직접 공지 작성/수정/삭제 | +80,000원 |
|
||||
| C. FAQ 관리 | 관리자가 FAQ 항목 추가/수정 | +50,000원 |
|
||||
| D. 영문 번역 대행 | 전 페이지 전문 번역 | +200,000원 |
|
||||
| E. 월 유지보수 | 월 2회 콘텐츠 수정 + 서버 관리 | 월 50,000원 |
|
||||
|
||||
---
|
||||
|
||||
## 견적 시나리오
|
||||
|
||||
### 시나리오 1: 기본 + 다국어 (추천)
|
||||
```
|
||||
기본 패키지: 250,000원
|
||||
한/영 전환: + 80,000원
|
||||
──────────────────────
|
||||
합계: 330,000원
|
||||
```
|
||||
|
||||
### 시나리오 2: 기본 + 다국어 + 관리자
|
||||
```
|
||||
기본 패키지: 250,000원
|
||||
한/영 전환: + 80,000원
|
||||
관리자 대시보드: +150,000원
|
||||
──────────────────────
|
||||
합계: 480,000원
|
||||
```
|
||||
|
||||
### 시나리오 3: 풀 패키지
|
||||
```
|
||||
기본 패키지: 250,000원
|
||||
한/영 전환: + 80,000원
|
||||
관리자 대시보드: +150,000원
|
||||
영문 번역: +200,000원
|
||||
──────────────────────
|
||||
합계: 680,000원
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 진행 일정
|
||||
|
||||
| 단계 | 기간 | 산출물 |
|
||||
|------|------|--------|
|
||||
| M1. 디자인 시안 | 2일 | 메인 페이지 시안 |
|
||||
| M2. 디자인 확정 | 1일 | 수정 반영 |
|
||||
| M3. 한글 퍼블리싱 | 3일 | 전 페이지 한글 버전 |
|
||||
| M4. 영문 적용 | 1일 | 다국어 전환 |
|
||||
| M5. QA + 배포 | 1일 | 최종 납품 |
|
||||
| **합계** | **8일** | |
|
||||
|
||||
---
|
||||
|
||||
## 계약 조건
|
||||
- 선금 50% / 잔금 50% (납품 완료 시)
|
||||
- 납기 지연 시 1일당 총금액 1% 차감
|
||||
- 납품 후 14일 무상 AS
|
||||
- 무상 수정 2회 (디자인 확정 후)
|
||||
- 소스코드 전체 이관
|
||||
1237
public/samples/hive-logistics-concept.html
Normal file
1237
public/samples/hive-logistics-concept.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user