mirror of
https://github.com/hangeol-chang/obsidian-csv-allinone.git
synced 2026-07-22 05:37:25 +00:00
Merge pull request #6 from Hangeol-Chang/FEAT-csv-write-applied
[FEAT, REFACTOR] header 구조 변경 및 일부 추가 기능 구현
This commit is contained in:
commit
40de65d4c5
7 changed files with 320 additions and 123 deletions
8
main.ts
8
main.ts
|
|
@ -28,14 +28,14 @@ export default class CsvPlugin extends Plugin {
|
|||
}
|
||||
|
||||
//// csvdisplay.ts
|
||||
openCsvInputModal = async (app: App, headers: Header[], fileName: string) => {
|
||||
createCsvInputModal_(app, headers, fileName);
|
||||
openCsvInputModal = async (app: App, headers: Header, fileName: string, defaultValues: {[key: string] : string} = {} ) => {
|
||||
createCsvInputModal_(app, headers, fileName, defaultValues);
|
||||
}
|
||||
createCsvTableView = (csvTable: CSVTable): HTMLElement => {
|
||||
return createCsvTableView_(csvTable);
|
||||
}
|
||||
createCsvFile = (filename: string, filePath: string, columnData: { name:string, type: string }[]) => {
|
||||
createCsvFile_(this.app, filename, filePath, columnData);
|
||||
createCsvFile = (filename: string, columnData: Header) => {
|
||||
createCsvFile_(this.app, filename, columnData);
|
||||
}
|
||||
|
||||
async onload() {
|
||||
|
|
|
|||
|
|
@ -1,38 +1,34 @@
|
|||
import { App, Modal, Notice, Plugin } from 'obsidian';
|
||||
import './styles/csvCreator.css';
|
||||
import { columnTypes } from './types';
|
||||
import { CSVCellType, CSVCellTypeString, Header } from './types';
|
||||
|
||||
// dataviewjs 등으로 자동적으로 파일을 생성하게 할 때 사용할 수 있는 api
|
||||
export const createCsvFile_ = (app: App, filename: string, filePath: string, columnData: { name:string, type: string }[]) : boolean => {
|
||||
export const createCsvFile_ = (app: App, filename: string, columnData: Header) : boolean => {
|
||||
// 수정해야함.
|
||||
// .csv 내용 생성 (헤더만 추가)
|
||||
const csvContent = columnData.map((col) => col.name).join(',') + '\n';
|
||||
let csvContent = '';
|
||||
for(const key of Object.keys(columnData)) {
|
||||
csvContent += key + ',';
|
||||
}
|
||||
csvContent = csvContent.slice(0, -1) + '\n';
|
||||
|
||||
// .csv.meta 내용 생성
|
||||
const metaContent = JSON.stringify(
|
||||
columnData.reduce((acc, col) => {
|
||||
acc[col.name] = col.type;
|
||||
return acc;
|
||||
}, {} as Record<string, string>),
|
||||
null,
|
||||
2
|
||||
);
|
||||
const metaContent = JSON.stringify(columnData, null, 2);
|
||||
|
||||
// 파일 저장
|
||||
const vault = app.vault;
|
||||
const fullPathCsv = `${filePath}/${filename}.csv`;
|
||||
const fullPathMeta = `${filePath}/${filename}.csv.meta`;
|
||||
|
||||
// .csv 파일 저장
|
||||
vault.adapter.write(fullPathCsv, csvContent).then(() => {
|
||||
new Notice(`${fullPathCsv} file has been created.`);
|
||||
vault.adapter.write(`${filename}.csv`, csvContent).then(() => {
|
||||
new Notice(`${filename}.csv file has been created.`);
|
||||
}).catch((err) => {
|
||||
new Notice(`Error occurred while creating .csv file:\n${err}`);
|
||||
return false;
|
||||
});
|
||||
|
||||
// .csv.meta 파일 저장
|
||||
vault.adapter.write(fullPathMeta, metaContent).then(() => {
|
||||
new Notice(`${fullPathMeta} file has been created.`);
|
||||
vault.adapter.write(`${filename}.csv.meta`, metaContent).then(() => {
|
||||
new Notice(`${filename}.csv.meta file has been created.`);
|
||||
}).catch((err) => {
|
||||
new Notice(`Error occurred while creating .csv.meta file:\n${err}`);
|
||||
return false;
|
||||
|
|
@ -44,25 +40,52 @@ export default class CsvCreateModal extends Modal {
|
|||
|
||||
columnsWrapper: HTMLElement;
|
||||
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
}
|
||||
constructor(app: App) { super(app); }
|
||||
|
||||
// column 추가
|
||||
addColumn() {
|
||||
let inputWarpper = this.columnsWrapper.createEl('tr');
|
||||
|
||||
let columnTd = inputWarpper.createEl('td');
|
||||
let columnInput = columnTd.createEl('input');
|
||||
columnInput.classList.add('colname-input');
|
||||
columnInput.id = 'colname';
|
||||
columnInput.placeholder = 'name';
|
||||
|
||||
let typeTd = inputWarpper.createEl('td');
|
||||
let typeSelect = typeTd.createEl('select');
|
||||
typeSelect.classList.add('coltype-select');
|
||||
for (let type of columnTypes) {
|
||||
typeSelect.id = 'coltype';
|
||||
for (let type of CSVCellTypeString) {
|
||||
let option = typeSelect.createEl('option');
|
||||
option.value = type;
|
||||
option.text = type;
|
||||
};
|
||||
|
||||
let defaultTd = inputWarpper.createEl('td');
|
||||
let defaultInput = defaultTd.createEl('input');
|
||||
defaultInput.classList.add('coldefault-input');
|
||||
defaultInput.id = 'coldefault';
|
||||
defaultInput.placeholder = 'default value';
|
||||
|
||||
// select type일 때만 options 추가
|
||||
let optionsInput = defaultTd.createEl('input');
|
||||
optionsInput.classList.add('coloptions-input');
|
||||
optionsInput.style.display = 'none';
|
||||
optionsInput.id = 'coloptions';
|
||||
optionsInput.placeholder = 'options (separated by comma)';
|
||||
// type가 select일 때만 css로 쿼리해서 보이게 할 것.
|
||||
|
||||
typeTd.addEventListener('change', () => {
|
||||
if(typeSelect.value === 'select') {
|
||||
optionsInput.style.display = 'block';
|
||||
defaultInput.style.display = 'none';
|
||||
}
|
||||
else {
|
||||
optionsInput.style.display = 'none';
|
||||
defaultInput.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createCsvFile() {
|
||||
|
|
@ -74,7 +97,7 @@ export default class CsvCreateModal extends Modal {
|
|||
const filePathInput = contentEl.querySelector('.filepath-input') as HTMLInputElement;
|
||||
|
||||
let filename = filenameInput.value.trim();
|
||||
let filePath = filePathInput.value.trim() || '/';
|
||||
let filePath = filePathInput.value.trim() || '';
|
||||
|
||||
// validation
|
||||
if (!filename) {
|
||||
|
|
@ -87,27 +110,54 @@ export default class CsvCreateModal extends Modal {
|
|||
if(filePath.length > 1 && filePath.endsWith('/')) {
|
||||
filePath = filePath.slice(0, -1);
|
||||
}
|
||||
let fullFilePath = `${filePath}/${filename}`;
|
||||
|
||||
// 컬럼 정보 수집
|
||||
const columnData: { name: string; type: string }[] = [];
|
||||
const columnData: Header = {};
|
||||
this.columnsWrapper.querySelectorAll('tr').forEach((columnEl) => {
|
||||
const inputEl = columnEl.querySelector('input') as HTMLInputElement;
|
||||
const selectEl = columnEl.querySelector('select') as HTMLSelectElement;
|
||||
const inputEl = columnEl.querySelector('#colname') as HTMLInputElement;
|
||||
const selectEl = columnEl.querySelector('#coltype') as HTMLSelectElement;
|
||||
const defaultEl = columnEl.querySelector('#coldefault') as HTMLInputElement;
|
||||
const optionsEl = columnEl.querySelector('#coloptions') as HTMLInputElement;
|
||||
|
||||
const columnName = inputEl?.value.trim();
|
||||
const columnType = selectEl?.value;
|
||||
let columnDefault : string | number = defaultEl?.value.trim();
|
||||
|
||||
if(columnDefault == '') {
|
||||
switch(columnType) {
|
||||
case 'number':
|
||||
columnDefault = 0;
|
||||
break;
|
||||
case 'stringDate':
|
||||
columnDefault = '1970-01-01';
|
||||
break;
|
||||
|
||||
case 'string':
|
||||
case 'select':
|
||||
case 'null':
|
||||
default:
|
||||
columnDefault = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (columnName && columnType) {
|
||||
columnData.push({ name: columnName, type: columnType });
|
||||
columnData[columnName] = {
|
||||
type: columnType as any,
|
||||
default: columnDefault,
|
||||
options:
|
||||
columnType === 'select'
|
||||
? optionsEl.value.split(',').map((option) => option.trim())
|
||||
: undefined
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (columnData.length === 0) {
|
||||
|
||||
if(Object.keys(columnData).length === 0) {
|
||||
new Notice('add at least one column');
|
||||
return false;
|
||||
}
|
||||
|
||||
return createCsvFile_(this.app, filename, filePath, columnData);
|
||||
return createCsvFile_(this.app, fullFilePath, columnData);
|
||||
}
|
||||
|
||||
// common api
|
||||
|
|
@ -135,14 +185,23 @@ export default class CsvCreateModal extends Modal {
|
|||
// column making table
|
||||
let columnsTable = contentEl.createEl('table');
|
||||
columnsTable.classList.add('columns-table');
|
||||
|
||||
let colgroup = columnsTable.createEl('colgroup');
|
||||
let nameCol = colgroup.createEl('col');
|
||||
nameCol.style.width = '25%';
|
||||
let typeCol = colgroup.createEl('col');
|
||||
typeCol.style.width = '25%';
|
||||
let defaultCol = colgroup.createEl('col');
|
||||
defaultCol.style.width = '50%';
|
||||
|
||||
let tHead = columnsTable.createEl('thead');
|
||||
let tHeadRow = tHead.createEl('tr');
|
||||
tHeadRow.classList.add('columns-table-header');
|
||||
|
||||
tHeadRow.createEl('th', {text: 'Column Name'});
|
||||
tHeadRow.createEl('th', {text: 'Type'});
|
||||
tHeadRow.createEl('th', {text: 'Default Value'});
|
||||
|
||||
columnsTable.createEl('hr');
|
||||
this.columnsWrapper = columnsTable.createEl('tbody');
|
||||
this.columnsWrapper.classList.add('columns-wrapper');
|
||||
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { CSVTable, Header } from './types'
|
||||
import { App, TFile } from 'obsidian';
|
||||
import { read } from 'fs';
|
||||
import { CSVTable, Header, isHeaderSame } from './types'
|
||||
import { App, Notice, TFile } from 'obsidian';
|
||||
|
||||
// meta file을 수정하는 로직이 포함되어야 함.
|
||||
|
||||
|
|
@ -10,31 +11,29 @@ export const readCSV_ = async (app: App, fileName: string): Promise<CSVTable | n
|
|||
const csvContent = await loadFile(app, fileName); // 파일 로드.
|
||||
|
||||
const lines = csvContent.split("\n").map(line => line.trim());
|
||||
const headersString = lines.shift()?.split(",") ?? [];
|
||||
const headersString: string[] = lines.shift()?.split(",") ?? [];
|
||||
const rows = lines.map(line => line.split(",").map(cell => cell.trim()));
|
||||
|
||||
// read .csv.meta file,
|
||||
// meta file 존재 여부 확인해서 없으면 생성.
|
||||
if(await app.vault.adapter.exists(`${fileName}.meta`) == false) {
|
||||
const headers: { name: string, type: string }[] =
|
||||
headersString.map((header) => ({ name: header, type: "string" }));
|
||||
|
||||
const metaContent = JSON.stringify(
|
||||
headers.reduce((acc, col) => {
|
||||
acc[col.name] = col.type;
|
||||
return acc;
|
||||
}, {} as Record<string, string>),
|
||||
null,
|
||||
2
|
||||
const metaContent: string = (
|
||||
JSON.stringify(headersString.reduce((meta, header: string) => {
|
||||
meta[header] = {
|
||||
type: "string",
|
||||
default: ""
|
||||
};
|
||||
return meta;
|
||||
}, {} as Header), null, 2)
|
||||
);
|
||||
// const metaContent = JSON.stringify(headers, null, 2);
|
||||
|
||||
await saveFile(app, `${fileName}.meta`, metaContent);
|
||||
new Notice(`meta file created : ${fileName}.meta`);
|
||||
}
|
||||
|
||||
|
||||
// meta file 로드.
|
||||
const metaData = await loadFile(app, `${fileName}.meta`); // 메타 파일 로드.
|
||||
const headers: Header[] = Object.entries(JSON.parse(metaData));
|
||||
const headers: Header = Header(metaData); // 메타 파일 파싱.
|
||||
|
||||
return await new CSVTable(headers, rows) // CSV 파싱.
|
||||
} else {
|
||||
|
|
@ -47,8 +46,9 @@ export const saveCSV_ = async (app: App, fileName: string, table: CSVTable): Pro
|
|||
// contents, meta로 분리 필요.
|
||||
const content = table.toCSV();
|
||||
await saveFile(app, fileName, content);
|
||||
// const meta = table.getMeta();
|
||||
// await saveMetaFile(app, `${fileName}.meta`, meta);
|
||||
|
||||
const header = table.getHeaders();
|
||||
await saveMetaFile(app, `${fileName}.meta`, header);
|
||||
} else {
|
||||
console.error(`file extension is not csv : ${fileName} (save)`);
|
||||
}
|
||||
|
|
@ -70,8 +70,14 @@ const saveFile = async (app: App, fileName: string, content: string): Promise<vo
|
|||
await vault.create(fileName, content);
|
||||
}
|
||||
}
|
||||
const saveMetaFile = async (app: App, fileName: string, content: Header[]): Promise<void> => {
|
||||
const saveMetaFile = async (app: App, fileName: string, header1: Header): Promise<void> => {
|
||||
const metaData = await loadFile(app, fileName); // 메타 파일 로드.
|
||||
const header2: Header = Header(metaData); // 메타 파일 파싱.
|
||||
|
||||
if(!isHeaderSame(header1, header2)) {
|
||||
const metaContent: string = JSON.stringify(header1, null, 2);
|
||||
await saveFile(app, fileName, metaContent);
|
||||
}
|
||||
}
|
||||
|
||||
const loadFile = async (app: App, fileName: string): Promise<string> => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { App, Modal } from "obsidian";
|
||||
import { CSVRow, CSVTable, Header } from "./types";
|
||||
import { CSVCellType, CSVRow, CSVTable, Header, HeaderContent } from "./types";
|
||||
import { readCSV_, saveCSV_ } from "./csvFilemanager";
|
||||
import './styles/csvPlugin.css';
|
||||
|
||||
export const createCsvInputModal_ = async (app: App, headers: Header[], fileName: string) => {
|
||||
const form = generateForm(fileName, headers);
|
||||
export const createCsvInputModal_ = async (app: App, headers: Header, fileName: string, defaultValues: {[key: string]: string}) => {
|
||||
const form = generateForm(fileName, headers, defaultValues);
|
||||
|
||||
const modal = new Modal(app);
|
||||
modal.contentEl.empty();
|
||||
|
|
@ -19,10 +19,10 @@ export const createCsvInputModal_ = async (app: App, headers: Header[], fileName
|
|||
const formData = new FormData(formElement);
|
||||
|
||||
const newRow: CSVRow = [];
|
||||
for(const header of headers) {
|
||||
newRow.push(formData.get(header[0]) as string);
|
||||
for(const key of Object.keys(headers)) {
|
||||
newRow.push(formData.get(key) as CSVCellType);
|
||||
}
|
||||
|
||||
|
||||
// 파일 읽기
|
||||
const table = await readCSV_(app, fileName);
|
||||
if(table === null) {
|
||||
|
|
@ -37,7 +37,7 @@ export const createCsvInputModal_ = async (app: App, headers: Header[], fileName
|
|||
});
|
||||
}
|
||||
|
||||
const generateForm = (fileName: string, headers: Header[]): HTMLFormElement => {
|
||||
const generateForm = (fileName: string, headers: Header, defaultValues: {[key: string]: string}): HTMLFormElement => {
|
||||
const form = document.createElement("form");
|
||||
form.id = "csv-input-form";
|
||||
|
||||
|
|
@ -57,10 +57,10 @@ const generateForm = (fileName: string, headers: Header[]): HTMLFormElement => {
|
|||
|
||||
// 폼 요소 생성
|
||||
// 입력 필드 생성 및 추가
|
||||
headers.forEach(header => {
|
||||
const field = createHeaderInputField(header);
|
||||
for(const [key, value] of Object.entries(headers)) {
|
||||
const field = createHeaderInputField(key, value, defaultValues[key] || "");
|
||||
form.appendChild(field);
|
||||
});
|
||||
};
|
||||
|
||||
// 버튼 그룹 생성 및 추가
|
||||
const buttonGroup = document.createElement("div");
|
||||
|
|
@ -77,7 +77,7 @@ const generateForm = (fileName: string, headers: Header[]): HTMLFormElement => {
|
|||
return form;
|
||||
}
|
||||
|
||||
const createHeaderInputField = (key: [string, string]): HTMLDivElement => {
|
||||
const createHeaderInputField = (key: string, headerContent: HeaderContent, defaultValue: string): HTMLDivElement => {
|
||||
// 입력 필드 래퍼 생성
|
||||
const formGroup = document.createElement("div");
|
||||
formGroup.className = "form-group";
|
||||
|
|
@ -85,20 +85,48 @@ const createHeaderInputField = (key: [string, string]): HTMLDivElement => {
|
|||
// 라벨 생성
|
||||
const label = document.createElement("label");
|
||||
label.className = "input-key";
|
||||
label.htmlFor = key[0];
|
||||
label.textContent = key[0];
|
||||
label.htmlFor = key;
|
||||
label.textContent = key;
|
||||
|
||||
formGroup.appendChild(label);
|
||||
|
||||
// 입력 필드 생성
|
||||
const input = document.createElement("input");
|
||||
input.className = "input-value";
|
||||
input.type = "text";
|
||||
input.id = key[0];
|
||||
input.name = key[0];
|
||||
input.placeholder = key[1];
|
||||
// select일 때와 아닐 때로 나눠야 함.
|
||||
|
||||
|
||||
|
||||
if(headerContent.type === "select") {
|
||||
const input = document.createElement("select");
|
||||
input.className = "input-value";
|
||||
input.id = key;
|
||||
input.name = key;
|
||||
|
||||
if(!(headerContent.options === undefined)) {
|
||||
// console.log(headerContent.options);
|
||||
for(const option of headerContent.options) {
|
||||
const optionElement = document.createElement("option");
|
||||
optionElement.value = option;
|
||||
optionElement.textContent = option;
|
||||
input.appendChild(optionElement);
|
||||
}
|
||||
}
|
||||
input.value = defaultValue;
|
||||
|
||||
formGroup.appendChild(input);
|
||||
}
|
||||
else {
|
||||
const input = document.createElement("input");
|
||||
input.className = "input-value";
|
||||
input.type = "text";
|
||||
input.id = key;
|
||||
input.name = key;
|
||||
input.placeholder = headerContent.default.toString();
|
||||
input.value = defaultValue;
|
||||
|
||||
formGroup.appendChild(input);
|
||||
}
|
||||
|
||||
// 요소 추가
|
||||
formGroup.appendChild(label);
|
||||
formGroup.appendChild(input);
|
||||
|
||||
return formGroup;
|
||||
}
|
||||
|
|
@ -142,11 +170,11 @@ export const createCsvTableView_ = (csvTable: CSVTable): HTMLElement => {
|
|||
// 테이블 헤더 생성
|
||||
const thead = document.createElement("thead");
|
||||
const headerRow = document.createElement("tr");
|
||||
headers.forEach(header => {
|
||||
for(const key of Object.keys(headers)) {
|
||||
const th = document.createElement("th");
|
||||
th.textContent = header[0];
|
||||
th.textContent = key;
|
||||
headerRow.appendChild(th);
|
||||
});
|
||||
}
|
||||
thead.appendChild(headerRow);
|
||||
table.appendChild(thead);
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ hr {
|
|||
.columns-table-header {
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #777;
|
||||
}
|
||||
|
||||
.colname-input {
|
||||
|
|
@ -73,3 +74,22 @@ hr {
|
|||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.coldefault-input {
|
||||
padding: 2px;
|
||||
width: 100%; height: 30px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 1rem;
|
||||
font-family: var(--font-monospace);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.coloptions-input {
|
||||
padding: 2px;
|
||||
width: 100%; height: 30px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 1rem;
|
||||
font-family: var(--font-monospace);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
148
src/types.ts
148
src/types.ts
|
|
@ -1,103 +1,163 @@
|
|||
export type CSVCell = string | number | null;
|
||||
export type stringDate = string; // 날짜를 string 형식 (YYYY-MM-DD)로 저장하기 위한 클래스
|
||||
function isStringDate(date: string): date is stringDate {
|
||||
const regex = /^\d{4}-\d{2}-\d{2}$/; // YYYY-MM-DD 형식 확인
|
||||
return regex.test(date);
|
||||
}
|
||||
export type select = string;
|
||||
|
||||
export type CSVCell = string | number | stringDate | select;
|
||||
export const CSVCellTypeString = ["string", "number", "stringDate", "select"] as const;
|
||||
export type CSVCellType = (typeof CSVCellTypeString)[number]; // 문자열 리터럴 타입 생성
|
||||
export type CSVRow = CSVCell[];
|
||||
export type Header = [string, string]; // [columnName, columnType]
|
||||
|
||||
export type HeaderContent = {
|
||||
type: CSVCellType;
|
||||
default: CSVCell;
|
||||
options?: string[]; // select type일 때만 필요
|
||||
};
|
||||
export type Header = {
|
||||
[key: string]: HeaderContent;
|
||||
}
|
||||
|
||||
export function Header(metaData: string): Header {
|
||||
const parsedData = JSON.parse(metaData) as Header; // 타입을 명시적으로 지정
|
||||
try {
|
||||
const headers: Header = Object.entries(parsedData).reduce((acc, [name, config]) => {
|
||||
acc[name] = {
|
||||
type: config.type as CSVCellType, // type이 정확히 무엇인지 명시
|
||||
default: config.default,
|
||||
...(config.options && { options: config.options }) // select일 때만 options 포함
|
||||
};
|
||||
return acc;
|
||||
}, {} as Header);
|
||||
|
||||
return headers;
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error("Invalid meta data.");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
export function isHeaderSame(header1: Header, header2: Header): boolean {
|
||||
if (Object.keys(header1).length !== Object.keys(header2).length) {
|
||||
return false;
|
||||
}
|
||||
for (const key in header1) {
|
||||
if (!header1.hasOwnProperty(key)) continue; // ✅ 상속된 속성 방지
|
||||
|
||||
// header2에 key가 없거나, type이 다르면 false
|
||||
if (!header2[key]?.type || header1[key].type !== header2[key].type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// select type일 때, options 비교
|
||||
if (header1[key].type === "select") {
|
||||
const opt1 = [...(header1[key].options ?? [])].sort();
|
||||
const opt2 = [...(header2[key].options ?? [])].sort();
|
||||
if (opt1.length !== opt2.length || !opt1.every((v, i) => v === opt2[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 두
|
||||
return true;
|
||||
}
|
||||
|
||||
export class CSVTable {
|
||||
private headers: Header[];
|
||||
private headers: Header; // Header는 이제 객체로 변경
|
||||
private rows: CSVRow[];
|
||||
|
||||
// string으로 넣거나, 분리된 headers/rows로 생성자 호출 가능.
|
||||
constructor(headers: Header[], rows: CSVRow[] = []) {
|
||||
constructor(headers: Header, rows: CSVRow[] = []) {
|
||||
this.headers = headers;
|
||||
this.rows = rows;
|
||||
}
|
||||
// constructor(contentOrHeaders: string | string[], rows: CSVRow[] = []) {
|
||||
// if (typeof contentOrHeaders === 'string') {
|
||||
// // content가 문자열일 때
|
||||
// const lines = contentOrHeaders.split("\n").map(line => line.trim());
|
||||
// this.headers = lines.shift()?.split(",") ?? [];
|
||||
// this.rows = lines.map(line => line.split(",").map(cell => cell.trim()));
|
||||
// } else {
|
||||
// // headers가 배열일 때
|
||||
// this.headers = contentOrHeaders;
|
||||
// this.rows = rows;
|
||||
// }
|
||||
// }
|
||||
|
||||
getHeaders(): Header[] {
|
||||
getHeaders(): Header {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
getRows(): CSVRow[] {
|
||||
return this.rows;
|
||||
}
|
||||
|
||||
// 가급적이면 CSVRow로 넣어주는 것을 권장.
|
||||
append(row: CSVRow | {[key: string]: string}): void {
|
||||
if (row.length !== this.headers.length) {
|
||||
throw new Error(`Row length (${row.length}) must match headers length (${this.headers.length}).`);
|
||||
append(row: CSVRow | { [key: string]: string }): void {
|
||||
if (Array.isArray(row)) {
|
||||
if (row.length !== Object.keys(this.headers).length) {
|
||||
throw new Error(`Row length (${row.length}) must match headers length.`);
|
||||
}
|
||||
this.rows.push(row);
|
||||
} else {
|
||||
// console.log(`append row: ${row}`);
|
||||
if(Array.isArray(row)) {
|
||||
this.rows.push(row);
|
||||
}
|
||||
else {
|
||||
// key-value로 들어올 때
|
||||
const newRow: CSVRow = [];
|
||||
for(const header of this.headers) { newRow.push(row[header[0]]); }
|
||||
this.rows.push(newRow);
|
||||
// key-value로 들어올 때
|
||||
const newRow: CSVRow = [];
|
||||
for (const key in this.headers) {
|
||||
if (row[key] === undefined) {
|
||||
throw new Error(`Missing value for column '${key}'.`);
|
||||
}
|
||||
newRow.push(row[key]);
|
||||
}
|
||||
this.rows.push(newRow);
|
||||
}
|
||||
}
|
||||
|
||||
delete(rowIndex: number): void {
|
||||
if (rowIndex < 0 || rowIndex >= this.rows.length) {
|
||||
throw new Error(`Invalid row index: ${rowIndex}`);
|
||||
}
|
||||
this.rows.splice(rowIndex, 1);
|
||||
}
|
||||
|
||||
updateCell(rowIndex: number, columnName: string, value: CSVCell): void {
|
||||
const columnIndex = this.headers.findIndex(([colname]) => colname === columnName);
|
||||
if (columnIndex === -1) {
|
||||
// Column name은 headers에서 찾고, 그에 맞는 인덱스를 찾아서 업데이트
|
||||
const header = this.headers[columnName];
|
||||
if (!header) {
|
||||
throw new Error(`Column '${columnName}' does not exist.`);
|
||||
}
|
||||
if (rowIndex < 0 || rowIndex >= this.rows.length) {
|
||||
throw new Error(`Invalid row index: ${rowIndex}`);
|
||||
}
|
||||
|
||||
const columnIndex = Object.keys(this.headers).indexOf(columnName);
|
||||
this.rows[rowIndex][columnIndex] = value;
|
||||
}
|
||||
|
||||
toCSV(): string {
|
||||
const csvContent = [this.headers.join(",")];
|
||||
const csvContent = [Object.keys(this.headers).join(",")];
|
||||
for (const row of this.rows) {
|
||||
csvContent.push(row.map(cell => (cell === null ? "" : cell.toString())).join(","));
|
||||
}
|
||||
return csvContent.join("\n");
|
||||
}
|
||||
|
||||
// 실제로 사용하게될 api일지는 모르겠습니다.
|
||||
addColoumn(columnName: string, columnType: string, value: CSVCell = 0): void {
|
||||
this.headers.push([columnName, columnType]);
|
||||
addColumn(columnName: string, columnType: CSVCellType, value: CSVCell = 0): void {
|
||||
this.headers[columnName] = { type: columnType, default: value };
|
||||
for (let i = 0; i < this.rows.length; i++) {
|
||||
this.rows[i].push(value);
|
||||
}
|
||||
}
|
||||
|
||||
deleteColumn(columnName: string): void {
|
||||
const columnIndex = this.headers.findIndex(([colname]) => colname === columnName);
|
||||
if (columnIndex === -1) {
|
||||
const header = this.headers[columnName];
|
||||
if (!header) {
|
||||
throw new Error(`Column '${columnName}' does not exist.`);
|
||||
}
|
||||
this.headers.splice(columnIndex, 1);
|
||||
delete this.headers[columnName];
|
||||
for (let i = 0; i < this.rows.length; i++) {
|
||||
const columnIndex = Object.keys(this.headers).indexOf(columnName);
|
||||
this.rows[i].splice(columnIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface CSVFile {
|
||||
name: string;
|
||||
path: string;
|
||||
content: CSVTable;
|
||||
}
|
||||
|
||||
export const columnTypes: string[] = [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
];
|
||||
// export const columnTypes: string[] = [
|
||||
// 'string',
|
||||
// 'number',
|
||||
// 'boolean',
|
||||
// ];
|
||||
24
test/.meta.json
Normal file
24
test/.meta.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"cost" : {
|
||||
"type" : "number",
|
||||
"default" : 0
|
||||
},
|
||||
"description" : {
|
||||
"type" : "string",
|
||||
"default" : "-"
|
||||
},
|
||||
"date" : {
|
||||
"type" : "string",
|
||||
"default" : "1970-01-01"
|
||||
},
|
||||
"category" : {
|
||||
"type" : "select",
|
||||
"default" : "other",
|
||||
"options" : [
|
||||
"food",
|
||||
"transport",
|
||||
"entertainment",
|
||||
"other"
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue