- 백엔드: 도메인 enum CodeEnum 상속(x-enum-varnames) → orval이 이름 있는 enum 생성. 응답 필드 enum 타입 지정(요청은 int 유지). ENUM_LABELS/DOMAIN_ENUMS·/v1/enums 제거 - 프론트: 생성 enum + 라벨맵으로 교체(매직넘버·발명 어휘 제거), 견적·세션 상태 코드화, role/delivery 라벨 프론트 소유 - 동반 정리: unwrap 캐스트·목업(buildSessions·데모)·死코드 제거, mapItem toMinPrice dedupe Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
181 lines
6.7 KiB
TypeScript
181 lines
6.7 KiB
TypeScript
import { Settings, Plus } from 'lucide-react';
|
|
import { useOverlayRouter } from '@/lib/useOverlayRouter';
|
|
import { PageContainer } from '@/components/layout/PageContainer';
|
|
import { PageToolbar, SearchInput } from '@/components/layout/PageToolbar';
|
|
import { TablePagination } from '@/components/ui/table-pagination';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { useServerList } from '@/lib/useServerList';
|
|
import { useQuotations } from '@/features/quotations/hooks/useQuotations';
|
|
import { useGetQuotation } from '@/api/generated/quotation/quotation';
|
|
import { QuotationTable } from '@/features/quotations/components/QuotationTable';
|
|
import { QuotationDetailSheet } from '@/features/quotations/components/QuotationDetailSheet';
|
|
import { QuotationCreateModal } from '@/features/quotations/components/QuotationCreateModal';
|
|
import { QuotationSettingsModal } from '@/features/quotations/components/QuotationSettingsModal';
|
|
import { QUOTATION_STATUS_OPTIONS, QUOTATION_TYPE_OPTIONS } from '@/features/quotations/types';
|
|
import type { ListQuotationsParams } from '@/api/generated/model/listQuotationsParams';
|
|
|
|
export default function QuotationPage() {
|
|
// 검색/상태·유형 필터/페이지 상태 → 서버 쿼리 파라미터로 변환.
|
|
const list = useServerList({ pageSize: 10, initialFilters: { status: 'ALL', type: 'ALL' } });
|
|
const statusFilter = list.filters.status;
|
|
const typeFilter = list.filters.type;
|
|
const statusOptions = QUOTATION_STATUS_OPTIONS;
|
|
const typeOptions = QUOTATION_TYPE_OPTIONS;
|
|
const params: ListQuotationsParams = {
|
|
search: list.debouncedSearch || undefined,
|
|
status: statusFilter !== 'ALL' ? statusFilter : undefined,
|
|
type: typeFilter !== 'ALL' ? typeFilter : undefined,
|
|
page: list.page,
|
|
size: list.pageSize,
|
|
};
|
|
|
|
const {
|
|
products,
|
|
partners,
|
|
cards,
|
|
quotations,
|
|
total,
|
|
quotationSettings,
|
|
closeQuotation,
|
|
addSetting,
|
|
deleteSetting,
|
|
createQuotation,
|
|
} = useQuotations(params);
|
|
const totalPages = list.totalPages(total);
|
|
|
|
const overlay = useOverlayRouter(['detail', 'create', 'settings']);
|
|
const detailId = overlay.get('detail');
|
|
const isCreateOpen = overlay.has('create');
|
|
const isSettingsOpen = overlay.has('settings');
|
|
|
|
// 상세 요약은 리스트에서 find 하지 않고 단건 API 로 받아온다(딥링크 시 리스트 의존 제거).
|
|
const detailQuery = useGetQuotation(detailId ?? '', { query: { enabled: !!detailId } });
|
|
const activeQuotation = detailQuery.data?.quotation ?? null;
|
|
|
|
return (
|
|
<PageContainer>
|
|
<PageToolbar
|
|
actions={
|
|
<>
|
|
<button
|
|
id="quotation-settings-btn"
|
|
onClick={() => overlay.open('settings')}
|
|
className="flex items-center gap-2 px-3 py-2.5 bg-muted text-foreground border border-border text-xs font-semibold rounded hover:bg-muted-foreground/10 cursor-pointer transition-colors"
|
|
>
|
|
<Settings size={14} />
|
|
<span>견적 세팅</span>
|
|
</button>
|
|
|
|
<button
|
|
id="quotation-create-btn"
|
|
onClick={() => overlay.open('create')}
|
|
className="flex items-center gap-2 px-4 py-2.5 bg-primary text-primary-foreground text-xs font-bold rounded hover:opacity-95 cursor-pointer transition-colors"
|
|
>
|
|
<Plus size={15} />
|
|
<span>신규 견적 등록</span>
|
|
</button>
|
|
</>
|
|
}
|
|
>
|
|
<SearchInput
|
|
id="quotation-search"
|
|
value={list.search}
|
|
onChange={(e) => list.setSearch(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && list.submitSearch()}
|
|
placeholder="견적명 또는 견적 번호로 검색..."
|
|
/>
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<Select value={statusFilter} onValueChange={(v) => list.setFilter('status', v as string)}>
|
|
<SelectTrigger id="quotation-status-filter" className="font-bold">
|
|
<SelectValue>
|
|
{(value) =>
|
|
value === 'ALL'
|
|
? '전체 견적상태'
|
|
: statusOptions.find((o) => String(o.value) === value)?.label ?? ''
|
|
}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="ALL">전체 견적상태</SelectItem>
|
|
{statusOptions.map((o) => (
|
|
<SelectItem key={o.value} value={String(o.value)}>{o.label}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Select value={typeFilter} onValueChange={(v) => list.setFilter('type', v as string)}>
|
|
<SelectTrigger id="quotation-type-filter">
|
|
<SelectValue>
|
|
{(value) =>
|
|
value === 'ALL'
|
|
? '전체 유형'
|
|
: typeOptions.find((o) => String(o.value) === value)?.label ?? ''
|
|
}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="ALL">전체 유형</SelectItem>
|
|
{typeOptions.map((o) => (
|
|
<SelectItem key={o.value} value={String(o.value)}>{o.label}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</PageToolbar>
|
|
|
|
<QuotationTable
|
|
data={quotations}
|
|
products={products}
|
|
onOpenDetail={(id) => overlay.open('detail', id)}
|
|
footer={
|
|
<TablePagination
|
|
page={list.page}
|
|
totalPages={totalPages}
|
|
totalCount={total}
|
|
pageSize={list.pageSize}
|
|
onPageChange={list.setPage}
|
|
label="전체 견적"
|
|
unit="건"
|
|
/>
|
|
}
|
|
/>
|
|
|
|
{activeQuotation && (
|
|
<QuotationDetailSheet
|
|
key={activeQuotation.qt_id}
|
|
quotation={activeQuotation}
|
|
onCloseQuotation={closeQuotation}
|
|
onClose={overlay.close}
|
|
/>
|
|
)}
|
|
|
|
{isCreateOpen && (
|
|
<QuotationCreateModal
|
|
open
|
|
products={products}
|
|
partners={partners}
|
|
cards={cards}
|
|
quotationSettings={quotationSettings}
|
|
onCreate={async (input) => {
|
|
const qtId = await createQuotation(input);
|
|
if (qtId) overlay.open('detail', qtId); // 생성 완료된 실제 qt_id 로 상세(협상현황) 자동 오픈
|
|
return !!qtId;
|
|
}}
|
|
onClose={overlay.close}
|
|
/>
|
|
)}
|
|
|
|
{isSettingsOpen && (
|
|
<QuotationSettingsModal
|
|
open
|
|
settings={quotationSettings}
|
|
onAdd={addSetting}
|
|
onDelete={deleteSetting}
|
|
onClose={overlay.close}
|
|
/>
|
|
)}
|
|
</PageContainer>
|
|
);
|
|
}
|