/** 수집해서는 안 되는 인스타그램의 예약어들. * 'p', 'reel'처럼 인스타그램 내부 시스템에서 쓰는 주소를 걸러내기 위함입니다. */ const RESERVED = new Set(['p', 'reel', 'reels', 'explore', 'stories', 'tv', 's', 'accounts']); /** * 입력된 텍스트(계정 ID, URL 등)를 실제 인스타그램 계정명으로 변환합니다. * @handle, URL, 공백 등을 제거하고 순수 계정명만 추출하며, 유효하지 않으면 null을 반환합니다. */ function toHandle(token: string): string | null { let t = token.trim(); if (!t) return null; // URL에서 계정 부분만 추출 (instagram.com/계정명) const m = t.match(/instagram\.com\/([^/?#]+)/i); if (m) t = m[1]!; // @ 기호 제거 및 소문자 변환 t = t.replace(/^@/, '').toLowerCase(); // 유효한 계정명 형식(영문/숫자/._)인지, 예약어는 아닌지 확인 if (!/^[a-z0-9._]{1,30}$/.test(t) || RESERVED.has(t)) return null; return t; } /** * 최소형 RFC4180 CSV 파서. * 엑셀이나 구글 스프레드시트에서 복사한 데이터를 처리하기 위해 쉼표와 따옴표를 정밀하게 파싱합니다. */ function parseCsv(text: string): string[][] { const s = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text; const rows: string[][] = []; let row: string[] = []; let cell = ''; let inQuotes = false; for (let i = 0; i < s.length; i += 1) { const c = s[i]!; if (inQuotes) { if (c === '"') { if (s[i + 1] === '"') { cell += '"'; i += 1; } else inQuotes = false; } else cell += c; } else if (c === '"') inQuotes = true; else if (c === ',') { row.push(cell); cell = ''; } else if (c === '\n' || c === '\r') { if (c === '\r' && s[i + 1] === '\n') i += 1; row.push(cell); rows.push(row); row = []; cell = ''; } else cell += c; } if (cell !== '' || row.length) { row.push(cell); rows.push(row); } return rows; } /** 채널 가져오기 형식 정의 */ export interface ChannelImport { username: string; category: string | null; } /** * 붙여넣기 하거나 CSV로 가져온 채널 목록을 '중복 없이' 정리합니다. * 구조화된 데이터(채널명/링크/카테고리 열)인지 확인하고 * 각 데이터의 링크를 우선순위로 하여 정확한 계정명만 추출합니다. */ export function parseChannelImport(raw: string): ChannelImport[] { return parseChannelImportWithSkipped(raw).items; } /** * 채널 목록을 파싱하되, 잘못된 형식(유효하지 않은 계정명 등)으로 * 걸러진 데이터까지 함께 반환하여 UI에 '어떤 부분이 빠졌는지' 알려줄 수 있게 합니다. */ export function parseChannelImportWithSkipped(raw: string): { items: ChannelImport[]; skipped: string[] } { const out: ChannelImport[] = []; const seen = new Set(); const skipped: string[] = []; // 데이터 추가 및 중복 검사 const push = (username: string | null, category: string | null): void => { if (!username || seen.has(username)) return; seen.add(username); out.push({ username, category }); }; // 잘못된 데이터 기록 const skip = (s: string): void => { const t = s.trim(); if (t && skipped.length < 100) skipped.push(t); }; // ... (이후 데이터 가공 로직 진행) }