Skip to content
26 changes: 24 additions & 2 deletions Koin/Data/DTOs/Decodable/Dining/DiningDto.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,20 @@ struct DiningDto: Decodable {
}

func toDomain() -> DiningItem {
return .init(id: id, type: type, place: place, priceCard: priceCard, priceCash: priceCash, kcal: kcal ?? 0, menu: menu ?? [], soldoutAt: soldoutAt, changedAt: changedAt, imageUrl: imageURL, likes: likes, isLiked: isLiked, date: date
return .init(
id: id,
type: type,
place: place,
priceCard: priceCard,
priceCash: priceCash,
kcal: kcal ?? 0,
menu: menu ?? [],
soldoutAt: soldoutAt,
changedAt: changedAt,
imageUrl: imageURL,
likes: likes,
isLiked: isLiked,
date: date
)
}
}
Expand All @@ -50,7 +63,16 @@ enum DiningType: String, Decodable {
let rawValue = try container.decode(String.self)
self = DiningType(rawValue: rawValue) ?? .breakfast
}


init?(segmentIndex: Int) {
switch segmentIndex {
case 0: self = .breakfast
case 1: self = .lunch
case 2: self = .dinner
default: return nil
}
}

var name: String {
switch self {
case .breakfast : "아침"
Expand Down
50 changes: 17 additions & 33 deletions Koin/Presentation/Dining/Dining/DiningViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ final class DiningViewController: UIViewController {
// MARK: - Properties

private let viewModel: DiningViewModel
private let inputSubject: PassthroughSubject<DiningViewModel.Input, Never> = .init()
private var subscriptions: Set<AnyCancellable> = []
let inputSubject: PassthroughSubject<DiningViewModel.Input, Never> = .init()
private let refreshControl = UIRefreshControl()
private var viewDidAppeared = false

Expand All @@ -33,7 +33,7 @@ final class DiningViewController: UIViewController {
}
}()

private let diningTypeSegmentControl = UISegmentedControl().then {
let diningTypeSegmentControl = UISegmentedControl().then {
$0.setBackgroundImage(UIImage(), for: .normal, barMetrics: .default)
$0.setDividerImage(UIImage(), forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default)
$0.insertSegment(withTitle: "아침", at: 0, animated: true)
Expand Down Expand Up @@ -62,7 +62,7 @@ final class DiningViewController: UIViewController {
$0.layer.applySketchShadow(color: .appColor(.neutral800), alpha: 0.02, x: 0, y: 1, blur: 1, spread: 0)
}

private let diningListCollectionView: DiningCollectionView = {
let diningListCollectionView: DiningCollectionView = {
let flowLayout = UICollectionViewFlowLayout().then {
$0.scrollDirection = .vertical
}
Expand Down Expand Up @@ -131,11 +131,7 @@ final class DiningViewController: UIViewController {
super.viewDidAppear(true)
checkAndShowBottomSheet()
if viewDidAppeared {
switch diningTypeSegmentControl.selectedSegmentIndex {
case 0: inputSubject.send(.updateDisplayDateTime(nil, .breakfast))
case 1: inputSubject.send(.updateDisplayDateTime(nil, .lunch))
default: inputSubject.send(.updateDisplayDateTime(nil, .dinner))
}
inputSubject.send(.updateDisplayDateTime(nil, currentDiningType))
}
viewDidAppeared = true

Expand All @@ -151,7 +147,7 @@ final class DiningViewController: UIViewController {
private func bind() {
let outputSubject = viewModel.transform(with: inputSubject.eraseToAnyPublisher())
outputSubject.receive(on: DispatchQueue.main).sink { [weak self] output in
guard let strongSelf = self else { return }
guard self != nil else { return }
switch output {
case let .updateDiningList(list, diningType):
self?.setDiningList(list)
Expand All @@ -177,7 +173,7 @@ final class DiningViewController: UIViewController {
zoomedImageViewController.setImage(tappedDiningImage)
self.present(zoomedImageViewController, animated: true, completion: nil)

inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuImage, .click, "\(self.getCurrentDiningType())_\(tappedPlaceText)"))
inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuImage, .click, "\(self.currentDiningType.name)_\(tappedPlaceText)"))

}.store(in: &subscriptions)

Expand All @@ -188,7 +184,7 @@ final class DiningViewController: UIViewController {

diningListCollectionView.logScrollPublisher.sink { [weak self] _ in
guard let self = self else { return }
self.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuTime, .scroll, self.getCurrentDiningType()))
self.inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuTime, .scroll, self.currentDiningType.name))
}.store(in: &subscriptions)

NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification).sink { [weak self] _ in
Expand All @@ -215,7 +211,7 @@ extension DiningViewController {
diningListCollectionView.addGestureRecognizer(swipeRightGesture)
}

@objc private func handleSwipe(_ gesture: UISwipeGestureRecognizer) {
@objc func handleSwipe(_ gesture: UISwipeGestureRecognizer) {
let currentSegmentIndex = diningTypeSegmentControl.selectedSegmentIndex
if gesture.direction == .left {
if currentSegmentIndex < diningTypeSegmentControl.numberOfSegments - 1 {
Expand All @@ -230,14 +226,9 @@ extension DiningViewController {
}

@objc private func refresh() {
switch diningTypeSegmentControl.selectedSegmentIndex {
case 0: inputSubject.send(.updateDisplayDateTime(nil, .breakfast))
case 1: inputSubject.send(.updateDisplayDateTime(nil, .lunch))
default: inputSubject.send(.updateDisplayDateTime(nil, .dinner))
}

inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuTime, .click, getCurrentDiningType()))

inputSubject.send(.updateDisplayDateTime(nil, currentDiningType))
inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuTime, .click, currentDiningType.name))

refreshControl.endRefreshing()
}

Expand Down Expand Up @@ -284,13 +275,10 @@ extension DiningViewController {
}

@objc private func segmentDidChange(_ sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0: inputSubject.send(.updateDisplayDateTime(nil, .breakfast))
case 1: inputSubject.send(.updateDisplayDateTime(nil, .lunch))
default: inputSubject.send(.updateDisplayDateTime(nil, .dinner))
}

inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuTime, .click, getCurrentDiningType()))
let diningType = DiningType(segmentIndex: sender.selectedSegmentIndex) ?? .dinner

inputSubject.send(.updateDisplayDateTime(nil, diningType))
inputSubject.send(.logEvent(EventParameter.EventLabel.Campus.menuTime, .click, diningType.name))
}

private func moveUnderLineView(diningType: DiningType) {
Expand Down Expand Up @@ -332,12 +320,8 @@ extension DiningViewController {
diningListCollectionView.setDiningList(list)
}

private func getCurrentDiningType() -> String {
switch diningTypeSegmentControl.selectedSegmentIndex {
case 0: return "아침"
case 1: return "점심"
default: return "저녁"
}
private var currentDiningType: DiningType {
DiningType(segmentIndex: diningTypeSegmentControl.selectedSegmentIndex) ?? .dinner
}
}

Expand Down
16 changes: 16 additions & 0 deletions koin.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,10 @@
AA20000000000000000004 /* DateProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000004 /* DateProviderTests.swift */; };
AA20000000000000000005 /* FetchDiningListUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000005 /* FetchDiningListUseCaseTests.swift */; };
AA20000000000000000006 /* ShareMenuListUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000006 /* ShareMenuListUseCaseTests.swift */; };
AA20000000000000000007 /* DiningLoggingTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000007 /* DiningLoggingTestSupport.swift */; };
AA20000000000000000008 /* DiningViewModelStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000008 /* DiningViewModelStubs.swift */; };
AA20000000000000000009 /* DiningLoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000009 /* DiningLoggingTests.swift */; };
AA20000000000000000010 /* DiningTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA10000000000000000010 /* DiningTypeTests.swift */; };
B438AEE42E756AB600E889C4 /* OrderImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B438AEE32E756AB600E889C4 /* OrderImage.swift */; };
B46B8CC22E76CB7300A8E797 /* ShopSummaryDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = B46B8CC12E76CB7300A8E797 /* ShopSummaryDto.swift */; };
B46B8CC42E76CC6300A8E797 /* FetchOrderShopSummaryFromShopUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B46B8CC32E76CC6300A8E797 /* FetchOrderShopSummaryFromShopUseCase.swift */; };
Expand Down Expand Up @@ -1744,6 +1748,10 @@
AA10000000000000000004 /* DateProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateProviderTests.swift; sourceTree = "<group>"; };
AA10000000000000000005 /* FetchDiningListUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchDiningListUseCaseTests.swift; sourceTree = "<group>"; };
AA10000000000000000006 /* ShareMenuListUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareMenuListUseCaseTests.swift; sourceTree = "<group>"; };
AA10000000000000000007 /* DiningLoggingTestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningLoggingTestSupport.swift; sourceTree = "<group>"; };
AA10000000000000000008 /* DiningViewModelStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningViewModelStubs.swift; sourceTree = "<group>"; };
AA10000000000000000009 /* DiningLoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningLoggingTests.swift; sourceTree = "<group>"; };
AA10000000000000000010 /* DiningTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningTypeTests.swift; sourceTree = "<group>"; };
B438AEE32E756AB600E889C4 /* OrderImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderImage.swift; sourceTree = "<group>"; };
B46B8CC12E76CB7300A8E797 /* ShopSummaryDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopSummaryDto.swift; sourceTree = "<group>"; };
B46B8CC32E76CC6300A8E797 /* FetchOrderShopSummaryFromShopUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchOrderShopSummaryFromShopUseCase.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4532,6 +4540,7 @@
children = (
AA10000000000000000001 /* PublisherTestSupport.swift */,
AA10000000000000000002 /* DiningFixtures.swift */,
AA10000000000000000007 /* DiningLoggingTestSupport.swift */,
);
path = Support;
sourceTree = "<group>";
Expand All @@ -4540,6 +4549,7 @@
isa = PBXGroup;
children = (
AA10000000000000000003 /* SpyDiningRepository.swift */,
AA10000000000000000008 /* DiningViewModelStubs.swift */,
);
path = Doubles;
sourceTree = "<group>";
Expand All @@ -4550,6 +4560,8 @@
AA10000000000000000004 /* DateProviderTests.swift */,
AA10000000000000000005 /* FetchDiningListUseCaseTests.swift */,
AA10000000000000000006 /* ShareMenuListUseCaseTests.swift */,
AA10000000000000000009 /* DiningLoggingTests.swift */,
AA10000000000000000010 /* DiningTypeTests.swift */,
);
path = Dining;
sourceTree = "<group>";
Expand Down Expand Up @@ -6628,6 +6640,10 @@
AA20000000000000000004 /* DateProviderTests.swift in Sources */,
AA20000000000000000005 /* FetchDiningListUseCaseTests.swift in Sources */,
AA20000000000000000006 /* ShareMenuListUseCaseTests.swift in Sources */,
AA20000000000000000007 /* DiningLoggingTestSupport.swift in Sources */,
AA20000000000000000008 /* DiningViewModelStubs.swift in Sources */,
AA20000000000000000009 /* DiningLoggingTests.swift in Sources */,
AA20000000000000000010 /* DiningTypeTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
102 changes: 102 additions & 0 deletions koinUnitTests/Dining/DiningLoggingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//
// DiningLoggingTests.swift
// koinUnitTests
//
// Created by 이은지 on 8/17/26.
//

import Foundation
import Testing
import UIKit
@testable import koin

@MainActor
@Suite("DiningViewController - 로깅")
struct DiningLoggingTests {

@Test(
"메뉴 이미지를 탭하면 시간대와 장소를 조합해 로깅한다",
arguments: [(0, "A코너", "아침_A코너"), (1, "B코너", "점심_B코너"), (2, "C코너", "저녁_C코너")]
)
func 메뉴_이미지를_탭하면_시간대와_장소를_조합해_로깅한다(segmentIndex: Int, place: String, expectedValue: String) {
let bed = DiningLoggingTestBed()
bed.selectSegment(segmentIndex)

bed.tapMenuImage(place: place)

#expect(bed.recorder.loggedEvents == [DiningLoggedEvent("menu_image", "click", expectedValue)])
}

@Test("공유 버튼을 탭하면 menuShare 이벤트를 로깅한다")
func 공유_버튼을_탭하면_menuShare_이벤트를_로깅한다() {
let bed = DiningLoggingTestBed()

bed.tapShareButton()

#expect(bed.recorder.loggedEvents == [DiningLoggedEvent("menu_share", "click", "공유하기")])
}

@Test("공유 버튼을 한 번 탭하면 공유와 로그가 각각 한 번씩 발행된다")
func 공유_버튼을_한_번_탭하면_공유와_로그가_각각_한_번씩_발행된다() {
let bed = DiningLoggingTestBed()

bed.tapShareButton()

#expect(bed.recorder.inputKinds == ["shareMenuList", "logEvent"])
}

@Test(
"리스트를 스크롤하면 현재 시간대로 menuTime을 로깅한다",
arguments: [(0, "아침"), (1, "점심"), (2, "저녁")]
)
func 리스트를_스크롤하면_현재_시간대로_menuTime을_로깅한다(segmentIndex: Int, expectedValue: String) {
let bed = DiningLoggingTestBed()
bed.selectSegment(segmentIndex)

bed.scrollDiningList()

#expect(bed.recorder.loggedEvents == [DiningLoggedEvent("menu_time", "scroll", expectedValue)])
}

@Test(
"당겨서 새로고침하면 현재 시간대로 menuTime을 로깅한다",
arguments: [(0, "아침"), (1, "점심"), (2, "저녁")]
)
func 당겨서_새로고침하면_현재_시간대로_menuTime을_로깅한다(segmentIndex: Int, expectedValue: String) {
let bed = DiningLoggingTestBed()
bed.selectSegment(segmentIndex)

bed.pullToRefresh()

#expect(bed.recorder.loggedEvents == [DiningLoggedEvent("menu_time", "click", expectedValue)])
}

@Test("학생식당 정보 버튼을 탭하면 cafeteriaInfo 이벤트를 로깅한다")
func 학생식당_정보_버튼을_탭하면_cafeteriaInfo_이벤트를_로깅한다() {
let bed = DiningLoggingTestBed()

bed.tapCafeteriaInfoButton()

#expect(bed.recorder.loggedEvents == [DiningLoggedEvent("cafeteria_info", "click", "학생식당정보")])
}

@Test("세그먼트를 전환하면 변경된 시간대로 menuTime을 로깅한다")
func 세그먼트를_전환하면_변경된_시간대로_menuTime을_로깅한다() {
let bed = DiningLoggingTestBed()
bed.selectSegment(0)

bed.tapSegment(1)

#expect(bed.recorder.loggedEvents == [DiningLoggedEvent("menu_time", "click", "점심")])
}

@Test("스와이프로 전환해도 동일하게 로깅한다")
func 스와이프로_전환해도_동일하게_로깅한다() {
let bed = DiningLoggingTestBed()
bed.selectSegment(1)

bed.swipe(.left)

#expect(bed.recorder.loggedEvents == [DiningLoggedEvent("menu_time", "click", "저녁")])
}
}
38 changes: 38 additions & 0 deletions koinUnitTests/Dining/DiningTypeTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// DiningTypeTests.swift
// koinUnitTests
//
// Created by 이은지 on 8/17/26.
//

import Foundation
import Testing
@testable import koin

@Suite("DiningType - 세그먼트 인덱스 변환")
struct DiningTypeTests {

@Test(
"세그먼트 인덱스를 시간대로 옮긴다",
arguments: [(0, DiningType.breakfast), (1, .lunch), (2, .dinner)]
)
func 세그먼트_인덱스를_시간대로_옮긴다(index: Int, expected: DiningType) {
#expect(DiningType(segmentIndex: index) == expected)
}

@Test(
"미선택이거나 범위 밖의 인덱스는 시간대를 특정하지 않는다",
arguments: [-1, 3, 100]
)
func 미선택이거나_범위_밖의_인덱스는_시간대를_특정하지_않는다(index: Int) {
#expect(DiningType(segmentIndex: index) == nil)
}

@Test(
"옮긴 시간대는 로깅에 쓰이는 한글 이름을 가진다",
arguments: [(0, "아침"), (1, "점심"), (2, "저녁")]
)
func 옮긴_시간대는_로깅에_쓰이는_한글_이름을_가진다(index: Int, expectedName: String) {
#expect(DiningType(segmentIndex: index)?.name == expectedName)
}
}
Loading