티스토리 뷰
사용자가 보유한 도메인들에 대한 정보들을 CSV 파일로 다운로드 받는 기능을 만들고자 한다.
CSV 파일로 만들 것이기 때문에 build.gradle 파일 dependencies에 아래와 같이 추가해준다.
dependencies {
implementation("com.opencsv:opencsv:4.4")
}
우선 컨트롤러를 만들고, 파일을 다운로드 할 것이니 때문에 따로 응답을 주는 것은 없다.
@RestController
@RequestMapping("/domains")
class Controller(
private val service: Service
) {
@GetMapping("/{userId}/csv")
fun download(
@PathVariable userId: String
): ResponseEntity<Unit> {
service.download(userId)
return ResponseEntity
.noContent()
.build()
}
}
서비스에서는 우선 도메인들에 대한 정보를 불러오고
파일 이름에 날짜를 표기할 것이기 때문에 UTC 형식으로 날짜 데이터를 가져왔다.
그런 다음에 Array로 첫 줄에는 컬럼명을 넣어주고,
그 뒤로 도메인들의 정보를 forEach를 이용해서 넣어주었다.
import com.opencsv.CSVWriter
import org.springframework.core.io.InputStreamResource
import java.io.*
import java.net.InetSocketAddress
import java.text.SimpleDateFormat
import java.util.*
@Service
class Service(
private val repository: Repository
) {
fun download(userId: String) {
val list = repository.findAllById(userId)
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS")
format.timeZone = TimeZone.getTimeZone("UTC")
val data = mutableListOf<Array<String>>().apply {
add(arrayOf(
"Domain", "Nameserver1", "Nameserver2", "Nameserver3", "Nameserver4",
"Nameserver5", "Registration Date", "Expiration Date"))
}
list?.forEach {
data.add(arrayOf(it.domain, it.nameserver1, it.nameserver2, it.nameserver3,
it.nameserver4, it.nameserver5, it.createdAt, it.expiredAt))
}
val date = Date()
val new = format.format(date)
try {
FileWriter(File(".Domains-$new.csv")).use { fw ->
CSVWriter(fw).use {
it.writeAll(data)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
우선 제대로 파일이 생성되는지 보기 위해
FileWriter를 이용해 CSV 파일을 생성해보았다.
여기서 핵심은 CSVWrite에 있다.
CSVWrite를 이용해서 파일을 생성해주면 CSV 파일 형식으로 떨굴 수 있다.
그런데 여기서 이제 일반 웹사이트를 통해 다운로드를 하면
기본 다운로드 폴더로 다운되도록 구현하고자 한다.
그럴 땐 이제 HttpServletResponse를 통해 가능하다.
컨트롤러에서 HttpServletResponse를 받아서
@RestController
@RequestMapping("/domains")
class Controller(
private val service: Service
) {
@GetMapping("/{userId}/csv")
fun download(
@PathVariable userId: String,
response: HttpServletResponse
): ResponseEntity<Unit> {
service.download(response, userId)
return ResponseEntity
.noContent()
.build()
}
}
아래처럼 Header에 설정 후 output 해주면 된다.
- response.contentType = "text/csv;charset=utf-8"
- response.setHeader("Content-Disposition", "attachment;filename=Domains-$new.csv")
- response.setHeader("Content-Transfer-Encoding", "binary")
import com.opencsv.CSVWriter
import org.springframework.core.io.InputStreamResource
import java.io.*
import java.net.InetSocketAddress
import java.text.SimpleDateFormat
import java.util.*
@Service
class Service(
private val repository: Repository
) {
fun download(response: HttpServletResponse, userId: String) {
val list = repository.findAllById(userId)
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS")
format.timeZone = TimeZone.getTimeZone("UTC")
val data = mutableListOf<Array<String>>().apply {
add(arrayOf(
"Domain", "Nameserver1", "Nameserver2", "Nameserver3", "Nameserver4",
"Nameserver5", "Registration Date", "Expiration Date"))
}
list?.forEach {
data.add(arrayOf(it.domain, it.nameserver1, it.nameserver2, it.nameserver3,
it.nameserver4, it.nameserver5, it.createdAt, it.expiredAt))
}
val date = Date()
val new = format.format(date)
response.contentType = "text/csv;charset=utf-8"
response.setHeader("Content-Disposition", "attachment;filename=Domains-$new.csv")
response.setHeader("Content-Transfer-Encoding", "binary")
var outputStream: OutputStream = response.outputStream
try {
outputStream = BufferedOutputStream(outputStream)
val outputStreamWriter = OutputStreamWriter(outputStream, "utf-8")
val csvWriter = CSVWriter(outputStreamWriter)
csvWriter.writeAll(data)
outputStreamWriter.flush()
} catch (e: Exception) {
e.printStackTrace()
} finally {
outputStream.flush()
outputStream.close()
}
}
}
그런데 문제가 생겼다.
CSV 파일을 엑셀에서 열면 한글이 깨진다.
utf-8로도, euc-kr로도 해봤는데 엑셀이 제대로 열리면 CSV 파일로 열었을 때 깨지고,
CSV 파일로 멀쩡하면 엑셀에서 깨지는 현상이 발생했다.
열심히 방법을 찾아도 며칠 동안 해결이 안됐었는데 겨우 찾은 정보로,
문서를 UTF-8-BOM 형식으로 저장하면 정상적으로 열 수 있다는 글을 발견했다.
파일 처음에 "\uFEFF"를 추가해주면 해당 파일이 UTF-8-BOM 형식으로 저장된다고 한다.
그렇게 최종적으로 작업된 내용은 아래와 같다.
import com.opencsv.CSVWriter
import org.springframework.core.io.InputStreamResource
import java.io.*
import java.net.InetSocketAddress
import java.text.SimpleDateFormat
import java.util.*
@Service
class Service(
private val repository: Repository
) {
fun download(domain: String) {
val list = repository.findAllById(domain)
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS")
format.timeZone = TimeZone.getTimeZone("UTC")
val data = mutableListOf<Array<String>>().apply {
add(arrayOf(
"Domain", "Nameserver1", "Nameserver2", "Nameserver3", "Nameserver4",
"Nameserver5", "Registration Date", "Expiration Date"))
}
list?.forEach {
data.add(arrayOf(it.domain, it.nameserver1, it.nameserver2, it.nameserver3,
it.nameserver4, it.nameserver5, it.createdAt, it.expiredAt))
}
val date = Date()
val new = format.format(date)
response.contentType = "text/csv;charset=utf-8"
response.setHeader("Content-Disposition", "attachment;filename=Domains-$new.csv")
response.setHeader("Content-Transfer-Encoding", "binary")
var outputStream: OutputStream = response.outputStream
try {
outputStream = BufferedOutputStream(outputStream)
val outputStreamWriter = OutputStreamWriter(outputStream, "utf-8")
outputStreamWriter.write("\uFEFF")
val csvWriter = CSVWriter(outputStreamWriter)
csvWriter.writeAll(data)
outputStreamWriter.flush()
} catch (e: Exception) {
e.printStackTrace()
} finally {
outputStream.flush()
outputStream.close()
}
}
}
이제 구현한 API를 프론트단에 붙이고 다운로드하면 이렇게 다운로드 창에 뜨고,
해당 파일을 CSV 파일로 열어봐도 한글도 제대로 나오고,
엑셀로도 정상 출력되는 것을 알 수 있다.
(참고 : https://jhdroid.tistory.com/11, https://velog.io/@oyeon/%ED%8C%8C%EC%9D%BC-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EA%B5%AC%ED%98%84, https://pygmalion0220.tistory.com/entry/Spring-boot-%ED%8C%8C%EC%9D%BC-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-%EB%8B%A4%EC%9A%B4, https://alakjkj.tistory.com/68)