Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,17 +126,15 @@ class ApplicationFormPdfAdapter : ApplicationFormPdfPort {
ByteArrayOutputStream().also { document.save(it) }.toByteArray()
}

override fun renderEssay(form: ApplicationForm, introduction: Boolean): ByteArray =
// 전체 원서와 같은 본문 배치를 사용하고 서식 3만 남긴다.
override fun renderEssay(form: ApplicationForm): ByteArray =
Loader.loadPDF(templateOf(form)).use { document ->
val page = document.pages[2]
document.pages.filterNot { it == page }.forEach(document::removePage)
document.essays(
page,
PDType0Font.load(document, fontFile.inputStream()),
form.copy(
introduction = form.introduction.takeIf { introduction },
studyPlan = form.studyPlan.takeUnless { introduction },
),
form,
ReceiptNumber.of(form.applicantId),
)
ByteArrayOutputStream().also { document.save(it) }.toByteArray()
Expand Down Expand Up @@ -297,7 +295,7 @@ private fun PDDocument.essays(page: PDPage, font: PDFont, form: ApplicationForm,
// 원본 칸 높이만큼 자리가 남으면 여기서 시작해 다음 장에 잇는다.
// 0.01pt 는 자기소개서 칸이 원본 그대로일 때 부동소수 오차로 학업계획서가 넘어가지 않게 봐준다.
if (STUDY_PLAN_BOTTOM - y - header + 0.01f < minOf(height(studyPlan), STUDY_PLAN_BOTTOM - STUDY_PLAN_TOP)) {
// 빈 학업계획서 칸 하나 때문에 장을 늘리지 않는다. 자기소개서만 뽑을 때(renderEssay)와 작성 중인 원서다.
// 빈 학업계획서 칸 하나 때문에 장을 늘리지 않는다.
if (studyPlan.isEmpty()) return
nextPage()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package hs.kr.entrydsm.application.administration.adapterout.document
import hs.kr.entrydsm.admin.domain.enum.ErrorCode
import hs.kr.entrydsm.admin.domain.exception.AdminDomainException
import hs.kr.entrydsm.admin.domain.port.out.AdmissionTicketPort
import hs.kr.entrydsm.admin.domain.port.out.ApplicationEssayPdfs
import hs.kr.entrydsm.admin.domain.port.out.ApplicationEssayPort
import hs.kr.entrydsm.admin.domain.port.out.EssayPdfMergePort
import hs.kr.entrydsm.application.administration.domain.document.exception.ApplicantNotFoundException
Expand All @@ -29,8 +28,9 @@ class LocalAdmissionTicketAdapter(private val documents: ApplicantFileUseCase) :
@Component
@ConditionalOnProperty(name = ["application.administration.enabled"], havingValue = "true")
class LocalApplicationEssayAdapter(private val documents: ApplicantFileUseCase) : ApplicationEssayPort {
override fun render(applicantId: Long, examineeNumber: String): ApplicationEssayPdfs = try {
documents.renderApplicationEssay(applicantId).let { ApplicationEssayPdfs(it.first, it.second) }
// 지원자별 통합 서식의 오류를 기존 관리자 오류 코드로 변환한다.
override fun render(applicantId: Long, examineeNumber: String): ByteArray? = try {
documents.renderApplicationEssay(applicantId)
} catch (error: Exception) {
throw renderFailure(error, ErrorCode.ESSAY_GENERATION_FAILED, listOf(applicantId))
}
Expand All @@ -39,14 +39,12 @@ class LocalApplicationEssayAdapter(private val documents: ApplicantFileUseCase)
@Component
@ConditionalOnProperty(name = ["application.administration.enabled"], havingValue = "true")
class PdfBoxEssayMergeAdapter : EssayPdfMergePort {
// 원본을 닫아도 글꼴과 리소스가 유지되도록 두 문서 전체를 복사한다.
override fun writeTo(documents: List<ApplicationEssayPdfs>, output: OutputStream) {
// 원본을 닫아도 글꼴과 리소스가 유지되도록 지원자별 문서 전체를 복사한다.
override fun writeTo(documents: List<ByteArray?>, output: OutputStream) {
PDDocument().use { merged ->
val merger = PDFMergerUtility()
documents.forEach { pdfs ->
listOfNotNull(pdfs.introduction, pdfs.studyPlan).forEach { pdf ->
Loader.loadPDF(pdf).use { source -> merger.appendDocument(merged, source) }
}
documents.filterNotNull().forEach { pdf ->
Loader.loadPDF(pdf).use { source -> merger.appendDocument(merged, source) }
}
merged.save(output)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,40 +66,37 @@ class ApplicationFormPdfTest {
val bytes = output.toByteArray()
System.getenv("TEST_UNDECLARED_OUTPUTS_DIR")?.let { File(it, "unissued-essays.pdf").writeBytes(bytes) }
Loader.loadPDF(bytes).use { document ->
assertEquals(2, document.numberOfPages)
val introduction = pageText(bytes, 1)
val plan = pageText(bytes, 2)
assertTrue(introduction, introduction.contains("저는 어려서부터"))
assertFalse(introduction.contains("입학 후에는"))
assertTrue(plan, plan.contains("입학 후에는"))
assertFalse(plan.contains("저는 어려서부터"))
assertEquals(1, document.numberOfPages)
val text = pageText(bytes, 1)
assertTrue(text, text.contains("저는 어려서부터"))
assertTrue(text, text.contains("입학 후에는"))
assertFalse(PDFTextStripper().getText(document).contains("미발급"))
}
}

@Test
fun `PDF 병합은 두 문서와 단독 학업계획서를 포함하고 빈 목록도 처리한다`() {
fun `PDF 병합은 지원자별 통합 문서를 포함하고 빈 목록도 처리한다`() {
fun pdf(pages: Int) = PDDocument().use { document ->
repeat(pages) { document.addPage(PDPage()) }
ByteArrayOutputStream().also { document.save(it) }.toByteArray()
}
val merger = PdfBoxEssayMergeAdapter()
val output = ByteArrayOutputStream()
merger.writeTo(listOf(
hs.kr.entrydsm.admin.domain.port.out.ApplicationEssayPdfs(pdf(1), pdf(2)),
hs.kr.entrydsm.admin.domain.port.out.ApplicationEssayPdfs(null, pdf(4)),
hs.kr.entrydsm.admin.domain.port.out.ApplicationEssayPdfs(null, null),
pdf(3),
pdf(4),
null,
), output)
Loader.loadPDF(output.toByteArray()).use { assertEquals(7, it.numberOfPages) }
val empty = ByteArrayOutputStream()
merger.writeTo(emptyList(), empty)
Loader.loadPDF(empty.toByteArray()).use { assertEquals(0, it.numberOfPages) }
org.junit.Assert.assertThrows(Exception::class.java) {
merger.writeTo(listOf(hs.kr.entrydsm.admin.domain.port.out.ApplicationEssayPdfs("broken pdf".toByteArray(), null)),
merger.writeTo(listOf("broken pdf".toByteArray()),
ByteArrayOutputStream())
}
org.junit.Assert.assertThrows(IllegalStateException::class.java) {
merger.writeTo(listOf(hs.kr.entrydsm.admin.domain.port.out.ApplicationEssayPdfs(pdf(1), null)),
merger.writeTo(listOf(pdf(1)),
object : java.io.OutputStream() { override fun write(value: Int) { error("storage failed") } })
}
}
Expand Down Expand Up @@ -326,9 +323,12 @@ class ApplicationFormPdfTest {
val shortStudyPlan = adapter.render(form().copy(introduction = essay.take(1200)), photo = null)
assertA4Pages(shortStudyPlan, FORM_COUNT)
assertTrue(pageText(shortStudyPlan, 3).contains("입학 후에는"))
// 자기소개서만 뽑을 때는 빈 학업계획서 칸 때문에 장을 늘리지 않는다. 학업계획서는 원본 칸 자리에서 다음 장으로 잇는다.
assertA4Pages(adapter.renderEssay(form().copy(introduction = essay), introduction = true), 1)
assertA4Pages(adapter.renderEssay(form().copy(studyPlan = essay), introduction = false), 2)
val combined = adapter.renderEssay(form().copy(introduction = essay, studyPlan = essay))
assertA4Pages(combined, 2)
val combinedBody = stamped(combined, 1).filter { it.yDirAdj > 278.16f } + stamped(combined, 2)
assertEquals(2 * essay.count { !it.isWhitespace() }, combinedBody.size)
combinedBody.forEach { assertEquals(10f, it.fontSizeInPt, 0.01f) }

}

@Test
Expand Down Expand Up @@ -389,29 +389,24 @@ class ApplicationFormPdfTest {
}

@Test
fun `자기소개서와 학업계획서를 서식 3 한 장씩 분리한다`() {
val introduction = adapter.renderEssay(form(), introduction = true)
val studyPlan = adapter.renderEssay(form(), introduction = false)

assertA4Pages(introduction, 1)
assertTrue(pageText(introduction, 1).contains("저는 어려서부터"))
assertTrue(!pageText(introduction, 1).contains("입학 후에는"))
assertA4Pages(studyPlan, 1)
assertTrue(pageText(studyPlan, 1).contains("입학 후에는"))
assertTrue(!pageText(studyPlan, 1).contains("저는 어려서부터"))
fun `자기소개서와 학업계획서를 지원자 원서와 같은 서식 3 한 장에 함께 출력한다`() {
val combined = adapter.renderEssay(form())
assertA4Pages(combined, 1)
assertTrue(pageText(combined, 1).contains("저는 어려서부터"))
assertTrue(pageText(combined, 1).contains("입학 후에는"))
val full = adapter.render(form(), photo = null)
assertEquals(stamped(full, 3).map { Triple(it.unicode, it.xDirAdj, it.yDirAdj) },
stamped(combined, 1).map { Triple(it.unicode, it.xDirAdj, it.yDirAdj) })
}

@Test
fun `검정고시 원서의 자기소개서와 학업계획서는 검정고시 서식 3 에 찍고 출신 중학교는 찍지 않는다`() {
val introduction = adapter.renderEssay(gedForm(), introduction = true)
val studyPlan = adapter.renderEssay(gedForm(), introduction = false)

assertA4Pages(introduction, 1)
assertTrue(pageText(introduction, 1).contains("저는 어려서부터"))
assertFalse(pageText(introduction, 1).contains("서귀포"))
assertA4Pages(studyPlan, 1)
assertTrue(pageText(studyPlan, 1).contains("입학 후에는"))
assertFalse(pageText(studyPlan, 1).contains("서귀포"))
fun `검정고시 통합 문서는 검정고시 서식 3 에 두 본문을 찍고 출신 중학교는 찍지 않는다`() {
val combined = adapter.renderEssay(gedForm())
assertA4Pages(combined, 1)
val text = pageText(combined, 1)
assertTrue(text.contains("저는 어려서부터"))
assertTrue(text.contains("입학 후에는"))
assertFalse(text.contains("서귀포"))
}

/** 칸 안에 기준선이 있는 원서 글자가 [count] 개이고, 모두 칸 좌우 안에 있다. 칸 아래로 넘친 글자는 수에서 빠진다. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class EssayPdfService(
private val applicationEssayPort: ApplicationEssayPort,
private val essayPdfMergePort: EssayPdfMergePort,
) : DownloadEssaysUseCase {
// 실제 1차 합격자의 두 서류를 수험번호 발급 여부와 무관하게 합친다.
// 실제 1차 합격자의 통합 서식을 수험번호 발급 여부와 무관하게 합친다.
override fun writeTo(output: OutputStream): Int {
val targets = applicantRepository.findAll(ApplicantFilter(statuses = setOf(ApplicantStatus.FIRST_PASS)))
.sortedWith(compareBy<Applicant> { it.examineeNumber.isNullOrBlank() }.thenBy { it.id })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,12 @@ class FileDocumentService(
return ticketStage("sheet_render", null) { admissionTicketSheetPort.render(prepared) }
}

override fun renderApplicationEssay(applicantId: Long): Pair<ByteArray?, ByteArray?> {
// 지원자 원서와 같은 서식에 두 본문을 함께 출력한다.
override fun renderApplicationEssay(applicantId: Long): ByteArray? {
val applicant = applicantPort.findById(applicantId) ?: throw ApplicantNotFoundException(applicantId)
val form = applicantPort.findApplicationForm(applicant.userId) ?: throw ApplicantNotFoundException(applicantId)
return form.introduction?.takeIf(String::isNotBlank)?.let { applicationFormPdfPort.renderEssay(form, true) } to
form.studyPlan?.takeIf(String::isNotBlank)?.let { applicationFormPdfPort.renderEssay(form, false) }
if (form.introduction.isNullOrBlank() && form.studyPlan.isNullOrBlank()) return null
return applicationFormPdfPort.renderEssay(form)
}

private fun ticket(applicantId: Long, applicant: Applicant, examineeNumber: String?): AdmissionTicket {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import hs.kr.entrydsm.admin.domain.enum.ApplicantStatus
import hs.kr.entrydsm.admin.domain.model.Applicant
import hs.kr.entrydsm.admin.domain.model.ApplicantFilter
import hs.kr.entrydsm.admin.domain.port.out.ApplicantRepository
import hs.kr.entrydsm.admin.domain.port.out.ApplicationEssayPdfs
import hs.kr.entrydsm.admin.domain.port.out.ApplicationEssayPort
import hs.kr.entrydsm.admin.domain.port.out.EssayPdfMergePort
import java.io.ByteArrayOutputStream
Expand All @@ -15,7 +14,7 @@ import org.junit.Test

class EssayZipServiceTest {
@Test
fun `합격자의 두 문서와 단독 학업계획서를 순서대로 병합기에 전달한다`() {
fun `합격자의 통합 문서를 순서대로 병합기에 전달한다`() {
val applicants = listOf(
Applicant(id = 1, examineeNumber = "11001", status = ApplicantStatus.FIRST_PASS),
Applicant(id = 2, examineeNumber = "11002", status = ApplicantStatus.FIRST_PASS),
Expand All @@ -25,17 +24,17 @@ class EssayZipServiceTest {
Applicant(id = 6, status = ApplicantStatus.PENDING),
)
val documents = listOf(
ApplicationEssayPdfs(byteArrayOf(1), null),
ApplicationEssayPdfs(byteArrayOf(2), byteArrayOf(3)),
ApplicationEssayPdfs(null, byteArrayOf(4)),
ApplicationEssayPdfs(null, null),
byteArrayOf(1),
byteArrayOf(2, 3),
byteArrayOf(4),
null,
)
val requested = mutableListOf<Pair<Long, String>>()
val output = ByteArrayOutputStream()
var merged = false
val service = EssayPdfService(repository(applicants), object : ApplicationEssayPort {
override fun render(applicantId: Long, examineeNumber: String) = error("batch 호출 필요")
override fun renderBatch(targets: List<Pair<Long, String>>): List<ApplicationEssayPdfs> {
override fun renderBatch(targets: List<Pair<Long, String>>): List<ByteArray?> {
requested += targets
return documents
}
Expand All @@ -54,7 +53,7 @@ class EssayZipServiceTest {
fun `대상이 없으면 빈 문서 목록을 병합기에 전달한다`() {
var merged = false
val service = EssayPdfService(repository(emptyList()), ApplicationEssayPort { _, _ -> error("호출되면 안 됨") },
EssayPdfMergePort { documents, _ -> assertEquals(emptyList<ApplicationEssayPdfs>(), documents); merged = true })
EssayPdfMergePort { documents, _ -> assertEquals(emptyList<ByteArray?>(), documents); merged = true })
assertEquals(0, service.writeTo(ByteArrayOutputStream()))
assertEquals(true, merged)
}
Expand All @@ -72,7 +71,7 @@ class EssayZipServiceTest {
)
val service = EssayPdfService(repository(applicants), ApplicationEssayPort { id, value ->
requested += id to value
ApplicationEssayPdfs(null, null)
null
}, EssayPdfMergePort { documents, _ -> assertEquals(4, documents.size) })
assertEquals(4, service.writeTo(ByteArrayOutputStream()))
assertEquals(if (issued) listOf(4L to " 11002 ", 9L to "11001", 3L to "", 7L to "")
Expand All @@ -84,7 +83,7 @@ class EssayZipServiceTest {
fun `병합 실패는 성공 처리하지 않고 원래 예외를 전달한다`() {
val failure = IllegalStateException("merge failed")
val service = EssayPdfService(repository(listOf(Applicant(id = 1, status = ApplicantStatus.FIRST_PASS))),
ApplicationEssayPort { _, _ -> ApplicationEssayPdfs(null, null) }, EssayPdfMergePort { _, _ -> throw failure })
ApplicationEssayPort { _, _ -> null }, EssayPdfMergePort { _, _ -> throw failure })
assertSame(failure, assertThrows(IllegalStateException::class.java) { service.writeTo(ByteArrayOutputStream()) })
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ import javax.imageio.ImageIO

class FileDocumentServiceTest {

@Test
fun `어드민 본문 출력은 두 본문을 함께 전달하고 한쪽만 있어도 출력하며 모두 공백이면 생략한다`() {
for ((introduction, studyPlan) in listOf("자기소개" to "학업계획", "자기소개" to null, null to "학업계획")) {
val form = applicationForm().copy(introduction = introduction, studyPlan = studyPlan)
forms[STUDENT_ID] = form
assertArrayEquals("%PDF-".toByteArray(), service.renderApplicationEssay(APPLICANT_ID))
assertEquals(form, formPdf.lastForm)
}
forms[STUDENT_ID] = applicationForm().copy(introduction = " \t", studyPlan = "\n")
formPdf.lastForm = null
assertNull(service.renderApplicationEssay(APPLICANT_ID))
assertNull(formPdf.lastForm)
}

@Test
fun `어드민 본문 출력은 지원자나 원서가 없으면 기존 조회 오류를 전달한다`() {
assertThrows(ApplicantNotFoundException::class.java) { service.renderApplicationEssay(999L) }
forms.clear()
assertThrows(ApplicantNotFoundException::class.java) { service.renderApplicationEssay(APPLICANT_ID) }
}

@Test
fun `수험표 단계 로그는 성공과 실패 위치를 기록하고 사진 키와 예외 메시지를 숨긴다`() {
val logger = org.slf4j.LoggerFactory.getLogger(FileDocumentService::class.java) as ch.qos.logback.classic.Logger
Expand Down Expand Up @@ -625,6 +646,11 @@ class FileDocumentServiceTest {
return "%PDF-".toByteArray()
}

override fun renderEssay(form: ApplicationForm): ByteArray {
lastForm = form
return "%PDF-".toByteArray()
}

override fun isRegistrationTemplate(template: ByteArray) = template.decodeToString().startsWith("%PDF-")

override fun renderRegistrationDocument(form: ApplicationForm, template: ByteArray): ByteArray {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ package hs.kr.entrydsm.admin.domain.port.out

import java.io.OutputStream

data class ApplicationEssayPdfs(val introduction: ByteArray?, val studyPlan: ByteArray?)

fun interface ApplicationEssayPort {
fun render(applicantId: Long, examineeNumber: String): ApplicationEssayPdfs
// 두 본문이 모두 비어 있으면 null을 반환한다.
fun render(applicantId: Long, examineeNumber: String): ByteArray?

fun renderBatch(targets: List<Pair<Long, String>>): List<ApplicationEssayPdfs> =
// 입력한 지원자 순서대로 통합 서식을 만든다.
fun renderBatch(targets: List<Pair<Long, String>>): List<ByteArray?> =
targets.map { (applicantId, examineeNumber) -> render(applicantId, examineeNumber) }
}

fun interface EssayPdfMergePort {
// 지원자별 자기소개서와 학업계획서를 받은 순서대로 합친다.
fun writeTo(documents: List<ApplicationEssayPdfs>, output: OutputStream)
// 두 본문을 함께 담은 지원자별 서식 3을 받은 순서대로 합친다.
fun writeTo(documents: List<ByteArray?>, output: OutputStream)
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface ApplicantFileUseCase {
*/
fun renderAdmissionTickets(tickets: List<Pair<Long, String?>>): ByteArray

fun renderApplicationEssay(applicantId: Long): Pair<ByteArray?, ByteArray?> =
/** 두 본문을 함께 담은 서식 3을 반환한다. 모두 비어 있으면 출력하지 않는다. */
fun renderApplicationEssay(applicantId: Long): ByteArray? =
throw UnsupportedOperationException()
}
Loading
Loading