DolphaGo / TIL

TIL & issues
0 stars 1 forks source link

[Vue3] CSV 다운로드 이슈 #107

Open DolphaGo opened 2 years ago

DolphaGo commented 2 years ago

Spring Server에서 OutputStream으로 데이터를 잘 넣고 있었으나, 받는 프론트 쪽에서 이슈가 있었다.

즉, request.getFile(url, filename) 으로 해결.

DolphaGo commented 2 years ago
import {Either, fail, success} from './either'
import params2query from './params-to-query'
import {NetworkError} from '../types/error'
import streamSaver from 'streamsaver'

export interface FetchRequestOptions {
  prefix: string
  headers: Record<string, string>
  params: Record<string, string | number | boolean>
}

export default class FetchRequest {
  private readonly defaultOptions: FetchRequestOptions = {
    prefix: '',
    headers: {},
    params: {},
  }

  private readonly options: FetchRequestOptions

  constructor (options: Partial<FetchRequestOptions> = {}) {
    this.options = Object.assign({}, this.defaultOptions, options)
  }

  private readonly generateFinalUrl = (url: string, options: Partial<FetchRequestOptions> = {}): string => {
    const prefix = options.prefix ?? this.options.prefix
    const params = Object.assign({}, this.options.params, options.params ?? {})

    let finalUrl = `${prefix}${url}`
    if (Object.keys(params).length > 0) finalUrl += `?${params2query(params)}`

    return finalUrl
  }

  private readonly generateFinalHeaders = (options: Partial<FetchRequestOptions> = {}): FetchRequestOptions['headers'] => {
    return Object.assign({}, this.options.headers, options.headers ?? {})
  }

  private readonly handleResponse = <T>(response: Response): Promise<Either<NetworkError, T>> => {
    if (response.ok) {
      return response.json().then(json => success(json as T))
    }

    return Promise.resolve(fail(new NetworkError(response)))
  }

  private readonly handleCorrectResponse = <T>(response: Response): Promise<T> => {
    if (response.ok) {
      return response.json()
    }

    throw new NetworkError(response)
  }

  private runFetch ({ method, url, data, options }: {
    method: 'GET' | 'DELETE' | 'POST' | 'PUT' | 'PATCH'
    url: string
    data?: unknown
    options?: Partial<FetchRequestOptions>
  }): Promise<Response> {
    const finalUrl = this.generateFinalUrl(url, options)
    const headers = this.generateFinalHeaders(options)

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const fetchOptions: any = { method, headers, mode: 'cors' }
    if (data !== undefined) fetchOptions.body = JSON.stringify(data)
    return fetch(finalUrl, fetchOptions)
  }

  private runFileDownloadFetch (
    method: 'GET' | 'POST',
    url: string,
    data?: unknown,
  ): Promise<Response> {
    const finalUrl = this.generateFinalUrl(url)
    const headers = { 'Content-Type': 'application/json; charset=UTF-8' }
    const fetchOptions: any = { method, headers, mode: 'cors' }
    if (data !== undefined) {
      fetchOptions.body = JSON.stringify(data)
    }
    return fetch(finalUrl, fetchOptions)
  }

  private runSafeFetch (
    method: 'GET' | 'DELETE',
    url: string,
    options?: Partial<FetchRequestOptions>,
  ): Promise<Response> {
    return this.runFetch({ method, url, options })
  }

  private runUnsafeFetch (
    method: 'POST' | 'PUT' | 'PATCH',
    url: string,
    data?: unknown,
    options?: Partial<FetchRequestOptions>,
  ): Promise<Response> {
    return this.runFetch({ method, url, options, data })
  }

  get<T = unknown> (url: string, options?: Partial<FetchRequestOptions>): Promise<T> {
    return this.runSafeFetch('GET', url, options).then(r => this.handleCorrectResponse<T>(r))
  }

  checkableGet<T = unknown> (url: string, options?: Partial<FetchRequestOptions>): Promise<Either<NetworkError, T>> {
    return this.runSafeFetch('GET', url, options).then(r => this.handleResponse<T>(r))
  }

  post<T = unknown> (url: string, data?: unknown, options?: Partial<FetchRequestOptions>): Promise<T> {
    return this.runUnsafeFetch('POST', url, data, options).then(r => this.handleCorrectResponse<T>(r))
  }

  postFile<T = unknown> (url: string, fileName: string, data?: unknown): void {
    const fileStream = streamSaver.createWriteStream(fileName)

    this.runFileDownloadFetch('POST', url, data)
      .then(res => {
        const readableStream = res.body

        // more optimized
        if (window.WritableStream && readableStream?.pipeTo) {
          return readableStream.pipeTo(fileStream)
            .then(() => console.log('done writing'))
        }

        const writer = fileStream.getWriter()
        const reader = res.body?.getReader()

        if (!reader) {
          throw new Error('stream reader initialize failed')
        }

        const pump = () => reader.read()
          .then(res => res.done
            ? writer.close()
            : writer.write(res.value).then(pump))

        pump().finally(() => console.log('pump : done writing'))
      })
      .catch(r => alert('error'))
  }

  getFile<T = unknown> (url: string, fileName: string, data?: unknown): void {
    const fileStream = streamSaver.createWriteStream(fileName)

    this.runFileDownloadFetch('GET', url, data)
        .then(res => {
          const readableStream = res.body

          // more optimized
          if (window.WritableStream && readableStream?.pipeTo) {
            return readableStream.pipeTo(fileStream)
                .then(() => console.log('done writing'))
          }

          const writer = fileStream.getWriter()
          const reader = res.body?.getReader()

          if (!reader) {
            throw new Error('stream reader initialize failed')
          }

          const pump = () => reader.read()
              .then(res => res.done
                  ? writer.close()
                  : writer.write(res.value).then(pump))

          pump().finally(() => console.log('pump : done writing'))
        })
        .catch(r => alert('error'))
  }

  checkablePost<T = unknown> (url: string, data?: unknown, options?: Partial<FetchRequestOptions>): Promise<Either<NetworkError, T>> {
    return this.runUnsafeFetch('POST', url, data, options).then(r => this.handleResponse<T>(r))
  }

  delete<T = unknown> (url: string, options?: Partial<FetchRequestOptions>): Promise<T> {
    return this.runSafeFetch('DELETE', url, options).then(r => this.handleCorrectResponse<T>(r))
  }

  checkableDelete<T = unknown> (url: string, options?: Partial<FetchRequestOptions>): Promise<Either<NetworkError, T>> {
    return this.runSafeFetch('DELETE', url, options).then(r => this.handleResponse<T>(r))
  }

  put<T = unknown> (url: string, data?: unknown, options?: Partial<FetchRequestOptions>): Promise<T> {
    return this.runUnsafeFetch('PUT', url, data, options).then(r => this.handleCorrectResponse<T>(r))
  }

  checkablePut<T> (url: string, data?: unknown, options?: Partial<FetchRequestOptions>): Promise<Either<NetworkError, T>> {
    return this.runUnsafeFetch('PUT', url, data, options).then(r => this.handleResponse<T>(r))
  }

  patch<T = unknown> (url: string, data?: unknown, options?: Partial<FetchRequestOptions>): Promise<T> {
    return this.runUnsafeFetch('PATCH', url, data, options).then(r => this.handleCorrectResponse<T>(r))
  }

  checkablePatch<T> (url: string, data?: unknown, options?: Partial<FetchRequestOptions>): Promise<Either<NetworkError, T>> {
    return this.runUnsafeFetch('PATCH', url, data, options).then(r => this.handleResponse<T>(r))
  }

  public setAuthorizationHeader (token: string): void {
    if (token !== '') this.options.headers.Authorization = `Token ${token}`
  }

  public deleteAuthorizationHeader (): void {
    delete this.options?.headers?.Authorization
  }
}