68f85f3736
- 첫 로그인 비번 강제 변경 (RUN_082): FORCE_PASSWORD_CHANGE 컬럼, ForcePasswordChangeGuardFilter, /auth/change-password API + 페이지 - 테넌트 일관성 가드: TenantConsistencyGuardFilter 로 JWT.company_code ↔ 서브도메인 company_code 대조, CompanyResolver 가 (db_name, company_code) 동시 반환 - 회사 관리 확장 (RUN_083 audit log, RUN_084 lifecycle 컬럼): CompanyAdmin/Members/Templates/Lifecycle/AuditLog 서비스 + CompanyMgmtController + SuperAdminGuard - 회사 관리 UI: CompanyAccordionRow 탭화 + 모달 4종 (AdminInfo/Deactivate/Delete/RecopyTemplates) + AuditLogDrawer + csvExport - 프로비저닝 마법사: force_password_change 토글 반영 - 프론트 인증: storage 이벤트 멀티탭 동기화, 403 errorCode (PASSWORD_CHANGE_REQUIRED / CROSS_TENANT_REJECTED / TENANT_NOT_RESOLVED) 전역 리다이렉트 - 기타: StartupSchemaMigrator, OS별 도커 기동 스크립트, CLAUDE.md 트래킹 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
44 lines
1.2 KiB
TypeScript
44 lines
1.2 KiB
TypeScript
/**
|
||
* 간이 CSV 변환 유틸 — client-side only.
|
||
* BOM 포함하여 엑셀에서 한글 깨짐 방지.
|
||
*/
|
||
|
||
export function toCsvString(
|
||
rows: Record<string, any>[],
|
||
columns: { key: string; label: string; format?: (v: any, row: Record<string, any>) => string }[],
|
||
): string {
|
||
const header = columns.map((c) => escapeCsv(c.label)).join(",");
|
||
const body = rows
|
||
.map((row) =>
|
||
columns
|
||
.map((c) => {
|
||
const raw = c.format ? c.format(row[c.key], row) : row[c.key];
|
||
return escapeCsv(raw);
|
||
})
|
||
.join(","),
|
||
)
|
||
.join("\n");
|
||
return "" + header + "\n" + body;
|
||
}
|
||
|
||
function escapeCsv(v: any): string {
|
||
if (v === null || v === undefined) return "";
|
||
const s = String(v);
|
||
if (/[",\n\r]/.test(s)) {
|
||
return `"${s.replace(/"/g, '""')}"`;
|
||
}
|
||
return s;
|
||
}
|
||
|
||
export function downloadCsv(filename: string, content: string) {
|
||
const blob = new Blob([content], { type: "text/csv;charset=utf-8;" });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement("a");
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
}
|