o2o-negosium-original/negodata/front/src/lib/excel.ts
Mina Choi 2a004734d8 [feat] negodata·negosium: 엑셀 데이터 내보내기·견적상세 협상카드 연동·드로어 뒤로가기
- 엑셀 데이터 내보내기(상품·협력사·협상카드): 현재 검색/페이지 필터를 무시하고 전체를 페이지 순회로 받아(fetchAllForExport, 무언 절삭 방지) 양식과 동일 헤더로 CSV 내보내기(downloadXxxData) + '데이터 다운로드' 메뉴·todayStamp 파일명 → 내려받아 수정 후 재업로드(라운드트립)
- 견적상세 ChatTab: 사용 협상카드 매칭을 chat_id→card_id(카드 PK)로 교정, 카드 사용 메시지 본문 script 중복 제거(카드 박스에만 노출), 카드칩→/cards?detail= 상세 링크, 카드 멘트 실가격 마스킹(target_price 변수 미주입+maskPrices)
- 견적상세 협상카드 탭: 번호·스크립트 미리보기 컬럼 추가(cardScriptPreview 평문 추출)·카드명 상세 링크
- 상세 드로어: 백드롭 좌상단 '뒤로'(navigate(-1)) 버튼 추가(데스크톱), 공용 Sheet·견적상세 동일 적용
- 협상요약(negosium): 배송 리드타임 라벨을 회사설정(labels.lead_time) 연동 → '표준납기' 표기
2026-07-30 11:03:51 +09:00

76 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 범용 엑셀(CSV) 내보내기. 클라이언트에서 Blob 다운로드 — 의존성 없음.
// 한글이 Excel에서 깨지지 않도록 UTF-8 BOM을 붙인다. 여러 페이지에서 재사용.
// 진짜 .xlsx(서식/다중시트)가 필요해지면 이 함수 시그니처 유지한 채 SheetJS로 교체 가능.
export type ExcelColumn<T> = {
header: string;
value: (row: T) => string | number | null | undefined;
};
// 일괄(엑셀) 등록 중 서버가 거부한 행. code 로 원본 행과 매칭해 사유를 표시한다.
export type BulkFailure = { code: string; message: string };
function escapeCell(v: string | number | null | undefined): string {
const s = v == null ? '' : String(v);
// 쉼표/따옴표/줄바꿈 포함 시 따옴표로 감싸고 내부 따옴표는 두 개로 이스케이프
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
}
// 파일명용 오늘 날짜(YYYYMMDD, 로컬 기준). 내보내기 파일명에 붙인다.
export function todayStamp(): string {
const d = new Date();
const p = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}`;
}
export function downloadExcel<T>(filename: string, columns: ExcelColumn<T>[], rows: T[]): void {
const header = columns.map((c) => escapeCell(c.header)).join(',');
const body = rows
.map((row) => columns.map((c) => escapeCell(c.value(row))).join(','))
.join('\r\n');
const csv = '' + header + '\r\n' + body; //  = UTF-8 BOM (Excel 한글 인코딩)
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename.endsWith('.csv') ? filename : `${filename}.csv`;
a.click();
URL.revokeObjectURL(url);
}
// CSV 토크나이저 — 따옴표 셀("..."), 이스케이프(""), CRLF/LF, 선두 BOM 처리.
function tokenizeCsv(text: string): string[][] {
const t = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text; // BOM 제거
const rows: string[][] = [];
let row: string[] = [];
let cell = '';
let inQuotes = false;
for (let i = 0; i < t.length; i++) {
const ch = t[i];
if (inQuotes) {
if (ch === '"') {
if (t[i + 1] === '"') { cell += '"'; i++; } // 이스케이프된 따옴표
else inQuotes = false;
} else cell += ch;
} else if (ch === '"') inQuotes = true;
else if (ch === ',') { row.push(cell); cell = ''; }
else if (ch === '\n') { row.push(cell); rows.push(row); row = []; cell = ''; }
else if (ch !== '\r') cell += ch;
}
if (cell !== '' || row.length > 0) { row.push(cell); rows.push(row); }
return rows;
}
// CSV 텍스트 → 헤더 키로 매핑된 객체 배열. 첫 행을 헤더로 본다. 여러 페이지 업로드에서 재사용.
export function parseCsv(text: string): Record<string, string>[] {
const rows = tokenizeCsv(text);
if (rows.length === 0) return [];
const headers = rows[0].map((h) => h.trim());
return rows
.slice(1)
.filter((cells) => cells.some((c) => c.trim() !== '')) // 빈 줄 제외
.map((cells) => Object.fromEntries(headers.map((h, i) => [h, (cells[i] ?? '').trim()])));
}