PageSourceSearch

https://factchecker.cloud/src/utils/fileUtils.ts

js factchecker.cloud collected 2026-10-02 01:21:49 UTC 5,699 bytes, 45 lines download raw bytes

1export function getFileExtension(filename) {
2	const parts = filename.split(".");
3	if (parts.length < 2) return null;
4	const ext = parts[parts.length - 1]?.toLowerCase();
5	return ext ? `.${ext}` : null;
6}
7/**
8* Blob 을 지정 파일명으로 브라우저 다운로드 트리거.
9*
10* Why: createObjectURL → anchor → append → click → remove → 지연 revoke 의식을
11* 7개 컴포넌트가 각자 복사하고 있었다(수정 시 7곳 동기화 필요). 단일 헬퍼로 통합.
12* - anchor 를 DOM 에 붙이는 이유: Firefox 일부 버전 + Safari 는 document tree 에
13*   없는 anchor 의 click() 이 download 속성을 무시하거나 no-op 이 되는 사례가 있다.
14* - revoke 를 60s 지연하는 이유: 같은 마이크로태스크 내 revoke 는 큰 파일(수십~수백 MB)
15*   에서 download fetch 시작 전에 URL 을 무효화해 0 byte 파일이 받히는 사례가 있다.
16* - window.document 사용: 호출부 컴포넌트에서 `document` 가 prop 으로 섀도잉되는
17*   경우(DocumentDetailModal)와 무관하게 안전.
18*/
19export function downloadBlob(blob, filename) {
20	const url = URL.createObjectURL(blob);
21	const a = window.document.createElement("a");
22	a.href = url;
23	a.download = filename;
24	window.document.body.appendChild(a);
25	a.click();
26	a.remove();
27	setTimeout(() => URL.revokeObjectURL(url), 6e4);
28}
29/**
30* Content-Disposition 헤더에서 파일명 — RFC 5987 `filename*=utf-8''…`(한글) 우선, 없으면 `filename=`.
31* zip 처럼 서버가 이름을 정하는 다운로드가 공유한다.
32*/
33export function filenameFromDisposition(header, fallback) {
34	const cd = String(header ?? "");
35	const star = /filename\*=utf-8''([^;]+)/i.exec(cd);
36	if (star) {
37		try {
38			return decodeURIComponent(star[1]);
39		} catch {}
40	}
41	const plain = /filename="?([^";]+)"?/i.exec(cd);
42	return plain ? plain[1] : fallback;
43}
44
45//# sourceMappingURL=data:application/json;base64,eyJtYXBwaW5ncyI6IkFBQUEsT0FBTyxTQUFTLGlCQUFpQixVQUFpQztDQUNoRSxNQUFNLFFBQVEsU0FBUyxNQUFNLEdBQUc7Q0FDaEMsSUFBSSxNQUFNLFNBQVMsR0FBRyxPQUFPO0NBQzdCLE1BQU0sTUFBTSxNQUFNLE1BQU0sU0FBUyxFQUFFLEVBQUUsWUFBWTtDQUNqRCxPQUFPLE1BQU0sSUFBSSxRQUFRO0FBQzNCOzs7Ozs7Ozs7Ozs7O0FBY0EsT0FBTyxTQUFTLGFBQWEsTUFBWSxVQUF3QjtDQUMvRCxNQUFNLE1BQU0sSUFBSSxnQkFBZ0IsSUFBSTtDQUNwQyxNQUFNLElBQUksT0FBTyxTQUFTLGNBQWMsR0FBRztDQUMzQyxFQUFFLE9BQU87Q0FDVCxFQUFFLFdBQVc7Q0FDYixPQUFPLFNBQVMsS0FBSyxZQUFZLENBQUM7Q0FDbEMsRUFBRSxNQUFNO0NBQ1IsRUFBRSxPQUFPO0NBQ1QsaUJBQWlCLElBQUksZ0JBQWdCLEdBQUcsR0FBRyxHQUFNO0FBQ25EOzs7OztBQU1BLE9BQU8sU0FBUyx3QkFBd0IsUUFBaUIsVUFBMEI7Q0FDakYsTUFBTSxLQUFLLE9BQU8sVUFBVSxFQUFFO0NBQzlCLE1BQU0sT0FBTyw2QkFBNkIsS0FBSyxFQUFFO0NBQ2pELElBQUksTUFBTTtFQUNSLElBQUk7R0FDRixPQUFPLG1CQUFtQixLQUFLLEVBQUU7RUFDbkMsUUFBUSxDQUVSO0NBQ0Y7Q0FDQSxNQUFNLFFBQVEseUJBQXlCLEtBQUssRUFBRTtDQUM5QyxPQUFPLFFBQVEsTUFBTSxLQUFLO0FBQzVCIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbImZpbGVVdGlscy50cyJdLCJ2ZXJzaW9uIjozLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZnVuY3Rpb24gZ2V0RmlsZUV4dGVuc2lvbihmaWxlbmFtZTogc3RyaW5nKTogc3RyaW5nIHwgbnVsbCB7XG4gIGNvbnN0IHBhcnRzID0gZmlsZW5hbWUuc3BsaXQoJy4nKTtcbiAgaWYgKHBhcnRzLmxlbmd0aCA8IDIpIHJldHVybiBudWxsO1xuICBjb25zdCBleHQgPSBwYXJ0c1twYXJ0cy5sZW5ndGggLSAxXT8udG9Mb3dlckNhc2UoKTtcbiAgcmV0dXJuIGV4dCA/IGAuJHtleHR9YCA6IG51bGw7XG59XG5cbi8qKlxuICogQmxvYiDsnYQg7KeA7KCVIO2MjOydvOuqheycvOuhnCDruIzrnbzsmrDsoIAg64uk7Jq066Gc65OcIO2KuOumrOqxsC5cbiAqXG4gKiBXaHk6IGNyZWF0ZU9iamVjdFVSTCDihpIgYW5jaG9yIOKGkiBhcHBlbmQg4oaSIGNsaWNrIOKGkiByZW1vdmUg4oaSIOyngOyXsCByZXZva2Ug7J2Y7Iud7J2EXG4gKiA36rCcIOy7tO2PrOuEjO2KuOqwgCDqsIHsnpAg67O17IKs7ZWY6rOgIOyeiOyXiOuLpCjsiJjsoJUg7IucIDfqs7Mg64+Z6riw7ZmUIO2VhOyalCkuIOuLqOydvCDtl6ztjbzroZwg7Ya17ZWpLlxuICogLSBhbmNob3Ig66W8IERPTSDsl5Ag67aZ7J2064qUIOydtOycoDogRmlyZWZveCDsnbzrtoAg67KE7KCEICsgU2FmYXJpIOuKlCBkb2N1bWVudCB0cmVlIOyXkFxuICogICDsl4bripQgYW5jaG9yIOydmCBjbGljaygpIOydtCBkb3dubG9hZCDsho3shLHsnYQg66y07Iuc7ZWY6rGw64KYIG5vLW9wIOydtCDrkJjripQg7IKs66GA6rCAIOyeiOuLpC5cbiAqIC0gcmV2b2tlIOulvCA2MHMg7KeA7Jew7ZWY64qUIOydtOycoDog6rCZ7J2AIOuniOydtO2BrOuhnO2DnOyKpO2BrCDrgrQgcmV2b2tlIOuKlCDtgbAg7YyM7J28KOyImOyLrX7siJjrsLEgTUIpXG4gKiAgIOyXkOyEnCBkb3dubG9hZCBmZXRjaCDsi5zsnpEg7KCE7JeQIFVSTCDsnYQg66y07Zqo7ZmU7ZW0IDAgYnl0ZSDtjIzsnbzsnbQg67Cb7Z6I64qUIOyCrOuhgOqwgCDsnojri6QuXG4gKiAtIHdpbmRvdy5kb2N1bWVudCDsgqzsmqk6IO2YuOy2nOu2gCDsu7Ttj6zrhIztirjsl5DshJwgYGRvY3VtZW50YCDqsIAgcHJvcCDsnLzroZwg7ISA64+E7J6J65CY64qUXG4gKiAgIOqyveyasChEb2N1bWVudERldGFpbE1vZGFsKeyZgCDrrLTqtIDtlZjqsowg7JWI7KCELlxuICovXG5leHBvcnQgZnVuY3Rpb24gZG93bmxvYWRCbG9iKGJsb2I6IEJsb2IsIGZpbGVuYW1lOiBzdHJpbmcpOiB2b2lkIHtcbiAgY29uc3QgdXJsID0gVVJMLmNyZWF0ZU9iamVjdFVSTChibG9iKTtcbiAgY29uc3QgYSA9IHdpbmRvdy5kb2N1bWVudC5jcmVhdGVFbGVtZW50KCdhJyk7XG4gIGEuaHJlZiA9IHVybDtcbiAgYS5kb3dubG9hZCA9IGZpbGVuYW1lO1xuICB3aW5kb3cuZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChhKTtcbiAgYS5jbGljaygpO1xuICBhLnJlbW92ZSgpO1xuICBzZXRUaW
451lb3V0KCgpID0+IFVSTC5yZXZva2VPYmplY3RVUkwodXJsKSwgNjBfMDAwKTtcbn1cblxuLyoqXG4gKiBDb250ZW50LURpc3Bvc2l0aW9uIO2XpOuNlOyXkOyEnCDtjIzsnbzrqoUg4oCUIFJGQyA1OTg3IGBmaWxlbmFtZSo9dXRmLTgnJ+KApmAo7ZWc6riAKSDsmrDshKAsIOyXhuycvOuptCBgZmlsZW5hbWU9YC5cbiAqIHppcCDsspjrn7wg7ISc67KE6rCAIOydtOumhOydhCDsoJXtlZjripQg64uk7Jq066Gc65Oc6rCAIOqzteycoO2VnOuLpC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGZpbGVuYW1lRnJvbURpc3Bvc2l0aW9uKGhlYWRlcjogdW5rbm93biwgZmFsbGJhY2s6IHN0cmluZyk6IHN0cmluZyB7XG4gIGNvbnN0IGNkID0gU3RyaW5nKGhlYWRlciA/PyAnJyk7XG4gIGNvbnN0IHN0YXIgPSAvZmlsZW5hbWVcXCo9dXRmLTgnJyhbXjtdKykvaS5leGVjKGNkKTtcbiAgaWYgKHN0YXIpIHtcbiAgICB0cnkge1xuICAgICAgcmV0dXJuIGRlY29kZVVSSUNvbXBvbmVudChzdGFyWzFdKTtcbiAgICB9IGNhdGNoIHtcbiAgICAgIC8vIOyemOuqu+uQnCAlLeyduOy9lOuUqeydtOuptCDslYTrnpggcGxhaW4g7J2066aE7Jy866GcXG4gICAgfVxuICB9XG4gIGNvbnN0IHBsYWluID0gL2ZpbGVuYW1lPVwiPyhbXlwiO10rKVwiPy9pLmV4ZWMoY2QpO1xuICByZXR1cm4gcGxhaW4gPyBwbGFpblsxXSA6IGZhbGxiYWNrO1xufVxuIl19

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.