o2o-negosium-original/negodata/front/src/pages/quotation.tsx
Mina Choi eb6612af1c [refactor] negodata: 도메인 코드값 enum 정비 — 백엔드 정본 + 프론트 생성타입 사용
- 백엔드: 도메인 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>
2026-06-22 17:06:26 +09:00

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>
);
}