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 @@ -3,79 +3,58 @@ package com.dmforu.crawling.parser
import com.dmforu.crawling.loader.HtmlLoader
import com.dmforu.domain.diet.Diet
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.time.LocalDate
import java.time.format.DateTimeFormatter

class DietParser (
class DietParser(
private val htmlLoader: HtmlLoader<Document>
) : Parser<Diet> {

override fun parse(): List<Diet> {
val document = htmlLoader.get(DMU_DIET_URL)
val menus = document.select(TABLE_SELECTOR)
return parseDietList(document)
}

private fun parseDietList(document: Document): List<Diet> {
val menus = document.select(MENU_SELECTOR)
val dates = document.select(DATE_SELECTOR)

val menuList = mutableListOf<String>()
val tests = mutableListOf<List<String>>()
val ms = menus[1].select("td")
for (m in ms) {
val menu : List<String> = m.text().substringAfter("[점심] ") ?.split(MENU_SEPARATOR)

val menuList = parseMenuList(menus)
val dateList = parseDateList(dates)

// 날짜와 메뉴 데이터를 조합하여 Diet 객체 생성
return dateList.zip(menuList)
.map { (date, menu) -> Diet.of(date, menu) }
}

private fun parseMenuList(menus: org.jsoup.select.Elements): List<List<String>> {
if (menus.size < 2) return emptyList()

return menus[1].select("td").map { element ->
element.text()
.substringAfter("[점심] ", "")
.takeIf { it.isNotBlank() }
?.split(MENU_SEPARATOR)
?.map { it.trim() }
?: emptyList()
tests.add(menu)
}

val dateList = mutableListOf<LocalDate>()
for (date in dates) {
val d = date.text().substringAfter("(").substringBefore(")")
val l = LocalDate.parse(d, DATE_FORMATTER)
dateList.add(l)
}

val result = mutableListOf<Diet>()

for (i: Int in 0 .. 4) {
val diet = Diet.of(dateList[i], tests[i])
result.add(diet)
}

return result
}

private fun parseDiet(row: Element): Diet? {
val columns = row.select(DATA_SELECTOR)

// 요일 출력
val day = columns[0].text()

// 짝수 컬럼에는 day 정보가 있는 위치에 "교직원식당"의 정보가 기입됨으로 넘겨야 함
if (PASS_COLUMN == day) {
return null

private fun parseDateList(dates: org.jsoup.select.Elements): List<LocalDate> {
return dates.map { dateElement ->
val dateText = dateElement.text()
.substringAfter("(")
.substringBefore(")")

LocalDate.parse(dateText, DATE_FORMATTER)
}

val parsedDate = LocalDate.parse(day.substring(0, 10), DATE_FORMATTER)

// 코리안 푸드 메뉴가 4번째 컬럼에 작성된다.
// 만일 식단의 작성 방법이 변경된다면 해당 로직 또한 변경의 필요성이 존재한다.
val menuColumn = columns.getOrNull(3)
val menuElement = menuColumn?.text()

// 메뉴가 공백이 아니면 메뉴를 분리하여 리스트로 변환
val menus = menuElement?.takeIf { it.isNotBlank() }
?.split(MENU_SEPARATOR)
?.map { it.trim() }
?: emptyList()

return Diet.of(parsedDate, menus)
}

companion object {
private val DATE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd")
private const val DMU_DIET_URL = "https://www.dongyang.ac.kr/dmu/4902/subview.do"
private const val TABLE_SELECTOR = "div.table_1 table tbody tr"
private const val MENU_SELECTOR = "div.table_1 table tbody tr"
private const val DATE_SELECTOR = "div.table_1 thead tr th"
private const val DATA_SELECTOR = "th, td"
private const val MENU_SEPARATOR = ", "
private const val PASS_COLUMN = "교직원식당"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.dmforu.crawling.parser


import com.dmforu.crawling.loader.HtmlLoader
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.tuple
Expand All @@ -9,123 +10,144 @@ import org.jsoup.select.Elements
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.BDDMockito.anyString
import org.mockito.BDDMockito.given
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.anyString
import org.mockito.Mockito.mock
import org.mockito.junit.jupiter.MockitoExtension
import java.time.LocalDate

//@ExtendWith(MockitoExtension::class)
//class DietParserTest {
//
// @Mock
// private lateinit var htmlLoader: HtmlLoader<Document>
//
// @InjectMocks
// private lateinit var dietParser: DietParser
//
// @DisplayName("식단표를 크롤링 할 수 있다.")
// @Test
// fun parse() {
// // given
// val mockDocument = mock(Document::class.java)
// val mockRows = Elements()
// val row1 = mock(Element::class.java)
// val row2 = mock(Element::class.java)
// val row3 = mock(Element::class.java)
// mockRows.add(row1)
// mockRows.add(row2)
// mockRows.add(row3)
// val columns1 = Elements(
// mock(Element::class.java).apply { given(text()).willReturn("2024.10.23") },
// mock(Element::class.java),
// mock(Element::class.java),
// mock(Element::class.java).apply { given(text()).willReturn("Menu 1, Menu 2") }
// )
// val columns2 = Elements(
// mock(Element::class.java).apply { given(text()).willReturn("교직원식당") }
// )
// val columns3 = Elements(
// mock(Element::class.java).apply { given(text()).willReturn("2024.10.24") },
// mock(Element::class.java),
// mock(Element::class.java),
// mock(Element::class.java).apply { given(text()).willReturn("Menu 3, Menu 4") }
// )
//
// given(htmlLoader.get(anyString())).willReturn(mockDocument)
// given(mockDocument.select(anyString())).willReturn(mockRows)
// given(row1.select(anyString())).willReturn(columns1)
// given(row2.select(anyString())).willReturn(columns2)
// given(row3.select(anyString())).willReturn(columns3)
//
// // when
// val result = dietParser.parse()
//
// // then
// assertThat(result).hasSize(2)
// .extracting("date", "menus")
// .containsExactly(
// tuple(LocalDate.of(2024, 10, 23), listOf("Menu 1", "Menu 2")),
// tuple(LocalDate.of(2024, 10, 24), listOf("Menu 3", "Menu 4")),
// )
// }
//
// @DisplayName("메뉴가 비어있다면 메뉴를 빈 리스트로 반환한다.")
// @Test
// fun parseWhenEmptyMenus() {
// // given
// val mockDocument = mock(Document::class.java)
// val mockRows = Elements()
// val row = mock(Element::class.java)
// mockRows.add(row)
// val columns = Elements(
// mock(Element::class.java).apply { given(text()).willReturn("2024.10.23") },
// mock(Element::class.java),
// mock(Element::class.java),
// mock(Element::class.java).apply { given(text()).willReturn(" ") }
// )
//
// given(htmlLoader.get(anyString())).willReturn(mockDocument)
// given(mockDocument.select(anyString())).willReturn(mockRows)
// given(row.select(anyString())).willReturn(columns)
//
// // when
// val result = dietParser.parse()
//
// // then
// assertThat(result).hasSize(1)
// val diet = result[0]
// assertThat(diet.date).isEqualTo(LocalDate.of(2024, 10, 23))
// assertThat(diet.menus).isEmpty()
// }
//
// @DisplayName("공휴일인 경우 메뉴를 빈 리스트로 반환한다.")
// @Test
// fun parseWhenHolidays() {
// // given
// val mockDocument = mock(Document::class.java)
// val mockRows = Elements()
// val row = mock(Element::class.java)
// mockRows.add(row)
// val columns = Elements(
// mock(Element::class.java).apply { given(text()).willReturn("2024.10.20") },
// mock(Element::class.java)
// )
//
// given(htmlLoader.get(anyString())).willReturn(mockDocument)
// given(mockDocument.select(anyString())).willReturn(mockRows)
// given(row.select(anyString())).willReturn(columns)
//
// // when
// val result = dietParser.parse()
//
// // then
// assertThat(result).hasSize(1)
// val diet = result[0]
// assertThat(diet.date).isEqualTo(LocalDate.of(2024, 10, 20))
// assertThat(diet.menus).isEmpty()
// }
//}
@ExtendWith(MockitoExtension::class)
class DietParserTest {

@Mock
private lateinit var htmlLoader: HtmlLoader<Document>

@InjectMocks
private lateinit var dietParser: DietParser

@DisplayName("식단표를 파싱할 수 있다")
@Test
fun parseNormalCase() {
// given
val mockDocument = mock(Document::class.java)

val menuRows = Elements(mock(Element::class.java), mock(Element::class.java))
val menuCells = Elements(
mock(Element::class.java).apply { given(text()).willReturn("[점심] 김치찌개, 된장국, 불고기") },
mock(Element::class.java).apply { given(text()).willReturn("[점심] 라면, 김밥, 떡볶이") }
)

val dateElements = Elements(
mock(Element::class.java).apply { given(text()).willReturn("월요일(2024.03.25)") },
mock(Element::class.java).apply { given(text()).willReturn("화요일(2024.03.26)") }
)

given(htmlLoader.get(anyString())).willReturn(mockDocument)
given(mockDocument.select("div.table_1 table tbody tr")).willReturn(menuRows)
given(mockDocument.select("div.table_1 thead tr th")).willReturn(dateElements)
given(menuRows[1].select("td")).willReturn(menuCells)

// when
val result = dietParser.parse()

// then
assertThat(result).hasSize(2)
.extracting("date", "menus")
.containsExactly(
tuple(LocalDate.of(2024, 3, 25), listOf("김치찌개", "된장국", "불고기")),
tuple(LocalDate.of(2024, 3, 26), listOf("라면", "김밥", "떡볶이"))
)
}

@DisplayName("메뉴가 비어있는 경우 빈 리스트로 처리한다")
@Test
fun parseEmptyMenu() {
// given
val mockDocument = mock(Document::class.java)

val menuRows = Elements(mock(Element::class.java), mock(Element::class.java))
val menuCells = Elements(
mock(Element::class.java).apply {
given(text()).willReturn("[점심] ")
}
)

val dateElements = Elements(
mock(Element::class.java).apply {
given(text()).willReturn("수요일(2024.03.27)")
}
)

given(htmlLoader.get(anyString())).willReturn(mockDocument)
given(mockDocument.select("div.table_1 table tbody tr")).willReturn(menuRows)
given(mockDocument.select("div.table_1 thead tr th")).willReturn(dateElements)
given(menuRows[1].select("td")).willReturn(menuCells)

// when
val result = dietParser.parse()

// then
assertThat(result).hasSize(1)
assertThat(result[0].date).isEqualTo(LocalDate.of(2024, 3, 27))
assertThat(result[0].menus).isEmpty()
}

@DisplayName("날짜와 메뉴의 수가 다른 경우 매칭되는 것만 처리한다")
@Test
fun parseWithDifferentSizes() {
// given
val mockDocument = mock(Document::class.java)

val menuRows = Elements(mock(Element::class.java), mock(Element::class.java))

val menuCells = Elements(
mock(Element::class.java).apply { given(text()).willReturn("[점심] 김치찌개, 된장국") }
)

val dateElements = Elements(
mock(Element::class.java).apply { given(text()).willReturn("금요일(2024.03.29)") },
mock(Element::class.java).apply { given(text()).willReturn("토요일(2024.03.30)") }
)

// Mock 동작 설정
given(htmlLoader.get(anyString())).willReturn(mockDocument)
given(mockDocument.select("div.table_1 table tbody tr")).willReturn(menuRows)
given(mockDocument.select("div.table_1 thead tr th")).willReturn(dateElements)
given(menuRows[1].select("td")).willReturn(menuCells)

// when
val result = dietParser.parse()

// then
assertThat(result).hasSize(1)
assertThat(result[0].date).isEqualTo(LocalDate.of(2024, 3, 29))
assertThat(result[0].menus).containsExactly("김치찌개", "된장국")
}

@DisplayName("메뉴 테이블이 없는 경우 빈 리스트를 반환한다")
@Test
fun parseWithNoMenuTable() {
// given
val mockDocument = mock(Document::class.java)

val emptyMenuRows = Elements()

val dateElements = Elements(
mock(Element::class.java).apply {
given(text()).willReturn("일요일(2024.03.31)")
}
)

given(htmlLoader.get(anyString())).willReturn(mockDocument)
given(mockDocument.select("div.table_1 table tbody tr")).willReturn(emptyMenuRows)
given(mockDocument.select("div.table_1 thead tr th")).willReturn(dateElements)

// when
val result = dietParser.parse()

// then
assertThat(result).isEmpty()
}
}
Loading