협상카드 - 목록/조회를 등록자 개인 → 회사(company.users 조인) 단위로 확장: 같은 회사 유저 카드 + 공용(user_id NULL) - 공용 카드는 기본 제공 자산 → 수정·삭제 불가(OWNER 포함), is_shared=True API 등록 거부 - 개인 카드 변경은 본인∪최고관리자만, 프론트 버튼 disable+툴팁 - 공개범위 셀렉터·엑셀 공개범위 컬럼·CardTable 전체 배지 제거(항상 개인 등록) - test_card.py 5케이스 재작성 견적 목표가 산정내역 - get_setting_rates: 단일 컬럼 select 결과를 이중 인덱싱해 항상 예외 → 마진율이 늘 0으로 적용되던 버그 수정 - get_target_breakdown: 채택 후보 판정을 '현재 최소값' → '저장 목표가와 값 일치'로 변경(산정 후 상품가 변동에도 초록 체크 유지) - 견적상세 협상카드 탭 세션ID 컬럼 제거 견적생성 모달 - 목표가 미리보기·마감기한 기본값(+1h)·미래 검증, combobox 팝오버 scrollIntoView, maskPrices 비율(%) 마스킹 Little-Helped: Stupid Claude
175 lines
7.0 KiB
TypeScript
175 lines
7.0 KiB
TypeScript
import { Plus, BookOpen, Upload, Download, FileSpreadsheet, ChevronDown } from 'lucide-react';
|
|
import { useOverlayRouter } from '@/lib/useOverlayRouter';
|
|
import { showToast } from '@/lib/notify';
|
|
import { confirm } from '@/lib/confirm';
|
|
import { PageContainer } from '@/components/layout/PageContainer';
|
|
import { SearchInput } from '@/components/layout/PageToolbar';
|
|
import { Button } from '@/components/ui/button';
|
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
|
import { TablePagination } from '@/components/ui/table-pagination';
|
|
import { Typography } from '@/components/ui/typography';
|
|
import { useServerList } from '@/lib/useServerList';
|
|
import { useCards } from '@/features/cards/hooks/useCards';
|
|
import { useGetCard } from '@/api/generated/card/card';
|
|
import { CardTable } from '@/features/cards/components/CardTable';
|
|
import { CardFormSheet } from '@/features/cards/components/CardFormSheet';
|
|
import { CardExcelUploadModal, downloadCardTemplate } from '@/features/cards/components/CardExcelUploadModal';
|
|
import { mapCardData, type CardTab, type NegotiationCard } from '@/features/cards/types';
|
|
import type { ListCardsParams } from '@/api/generated/model/listCardsParams';
|
|
|
|
export default function CardsPage() {
|
|
// 검색/탭/페이지 상태(재사용 훅) → 서버 쿼리 파라미터로 변환.
|
|
const list = useServerList({ pageSize: 10, initialFilters: { tab: 'ALL' } });
|
|
const activeTab = list.filters.tab as CardTab;
|
|
const params: ListCardsParams = {
|
|
search: list.debouncedSearch || undefined,
|
|
is_wildcard: activeTab === 'ALL' ? undefined : activeTab === 'WILD',
|
|
page: list.page,
|
|
size: list.pageSize,
|
|
};
|
|
const { cards, total, totalNego, totalWild, createCard, updateCard, deleteCard, bulkCreate } = useCards(params);
|
|
const totalPages = list.totalPages(total);
|
|
|
|
// 오버레이(폼/엑셀)를 쿼리스트링으로 → 딥링크·뒤로가기·새로고침 지원.
|
|
// ?detail=<id> 직접 접근 시 단건 API 로 받아 수정 폼을 연다(현재 페이지에 없어도 동작).
|
|
const overlay = useOverlayRouter(['new', 'detail', 'modal']);
|
|
const editId = overlay.get('detail');
|
|
const modal = overlay.get('modal'); // 'excel' | null
|
|
const editQuery = useGetCard(editId ?? '', { query: { enabled: !!editId } });
|
|
const editing: NegotiationCard | null = editQuery.data?.card ? mapCardData(editQuery.data.card) : null;
|
|
const formMode: 'create' | 'edit' = editId ? 'edit' : 'create';
|
|
const isFormOpen = overlay.has('new') || !!editing;
|
|
|
|
const openCreate = () => overlay.open('new');
|
|
const openEdit = (card: NegotiationCard) => overlay.open('detail', card.id);
|
|
|
|
const handleDeleteCard = async (id: string, cardName: string) => {
|
|
if (await confirm({ title: '카드 삭제', description: `[${cardName}]을 삭제하시겠습니까?`, confirmText: '삭제', destructive: true })) {
|
|
try {
|
|
await deleteCard(id);
|
|
showToast('삭제되었습니다.', 'info');
|
|
} catch (err) {
|
|
showToast(err instanceof Error ? err.message : '카드 삭제 실패', 'error');
|
|
}
|
|
}
|
|
};
|
|
|
|
const tabs: { id: CardTab; label: string; count: number }[] = [
|
|
{ id: 'ALL', label: '전체', count: totalNego + totalWild },
|
|
{ id: 'CARD', label: '협상카드', count: totalNego },
|
|
{ id: 'WILD', label: '와일드카드', count: totalWild },
|
|
];
|
|
|
|
return (
|
|
<PageContainer>
|
|
{/* Upper Information Banner */}
|
|
<div className="p-4 rounded-lg border border-border bg-card flex flex-col sm:flex-row items-center justify-between gap-4 font-mono text-xs">
|
|
<div className="flex items-center gap-3">
|
|
<BookOpen className="text-muted-foreground" size={20} />
|
|
<div>
|
|
<Typography variant="h3" className="text-xs">협상카드 및 와일드카드 관리</Typography>
|
|
<Typography variant="muted" className="text-[10px] mt-0.5">
|
|
우리 회사 카드와 기본 제공(공용) 카드를 함께 조회합니다. 공용 카드는 수정·삭제할 수 없습니다.
|
|
</Typography>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger render={<Button variant="outline" />}>
|
|
<FileSpreadsheet />
|
|
엑셀업로드
|
|
<ChevronDown />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuItem onClick={() => overlay.open('modal', 'excel')}>
|
|
<Upload />
|
|
일괄 업로드
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={downloadCardTemplate}>
|
|
<Download />
|
|
양식 다운로드
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
<button
|
|
id="card-register-btn"
|
|
onClick={openCreate}
|
|
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 whitespace-nowrap"
|
|
>
|
|
<Plus size={15} />
|
|
<span>신규 카드 등록</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Primary Navigation Tab */}
|
|
<div className="flex border-b border-border">
|
|
{tabs.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
id={`card-tab-${tab.id.toLowerCase()}`}
|
|
onClick={() => list.setFilter('tab', tab.id)}
|
|
className={`py-2 px-6 font-bold text-xs tracking-tight border-b-2 transition-all cursor-pointer ${
|
|
activeTab === tab.id
|
|
? 'border-primary text-primary'
|
|
: 'border-transparent text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
{tab.label} ({tab.count})
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Filtering Search Bar */}
|
|
<SearchInput
|
|
id="card-search"
|
|
value={list.search}
|
|
onChange={(e) => list.setSearch(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && list.submitSearch()}
|
|
onClear={list.clearSearch}
|
|
placeholder="전체 카드이름, 카드번호, 코드 검색..."
|
|
/>
|
|
|
|
<CardTable
|
|
data={cards}
|
|
onEdit={openEdit}
|
|
footer={
|
|
<TablePagination
|
|
page={list.page}
|
|
totalPages={totalPages}
|
|
totalCount={total}
|
|
pageSize={list.pageSize}
|
|
onPageChange={list.setPage}
|
|
label="전체 카드"
|
|
unit="개"
|
|
/>
|
|
}
|
|
/>
|
|
|
|
{isFormOpen && (
|
|
<CardFormSheet
|
|
key={`${formMode}-${editing?.id ?? 'new'}`}
|
|
open
|
|
mode={formMode}
|
|
card={editing}
|
|
activeTab={activeTab}
|
|
onCreate={createCard}
|
|
onUpdate={updateCard}
|
|
onDelete={handleDeleteCard}
|
|
onClose={overlay.close}
|
|
/>
|
|
)}
|
|
|
|
{modal === 'excel' && (
|
|
<CardExcelUploadModal
|
|
open
|
|
onConfirm={bulkCreate}
|
|
onClose={overlay.close}
|
|
/>
|
|
)}
|
|
</PageContainer>
|
|
);
|
|
}
|