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
24 changes: 22 additions & 2 deletions Projects/App/Sources/MainTab/MainTabFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import FortuneFeature
import Foundation
import LuckyActionFeature
import MyPageFeature
import TodakFeature

@Reducer
struct MainTabFeature {
Expand All @@ -20,7 +21,9 @@ struct MainTabFeature {
@ObservableState
struct State: Equatable {
var selectedTab: Tab
var previousTab: Tab
var fortune: FortuneFeature.State
var todak: TodakFeature.State
var luckyAction: LuckyActionFeature.State
@Presents var pushedLuckyAction: LuckyActionFeature.State?
var myPage: MyPageFeature.State
Expand All @@ -30,13 +33,17 @@ struct MainTabFeature {

init(
selectedTab: Tab = .fortune,
previousTab: Tab = .fortune,
fortune: FortuneFeature.State = .init(),
todak: TodakFeature.State = .init(),
luckyAction: LuckyActionFeature.State = .init(),
pushedLuckyAction: LuckyActionFeature.State? = nil,
myPage: MyPageFeature.State = .init()
) {
self.selectedTab = selectedTab
self.previousTab = previousTab
self.fortune = fortune
self.todak = todak
self.luckyAction = luckyAction
self.pushedLuckyAction = pushedLuckyAction
self.myPage = myPage
Expand All @@ -47,6 +54,7 @@ struct MainTabFeature {
case selectedTabChanged(Tab)
case fortuneNavigationChanged
case fortune(FortuneFeature.Action)
case todak(TodakFeature.Action)
case luckyAction(LuckyActionFeature.Action)
case pushedLuckyAction(PresentationAction<LuckyActionFeature.Action>)
case pushedLuckyActionPresentationChanged(Bool)
Expand All @@ -62,13 +70,20 @@ struct MainTabFeature {
MyPageFeature()
}

Scope(state: \.todak, action: \.todak) {
TodakFeature()
}

Scope(state: \.luckyAction, action: \.luckyAction) {
LuckyActionFeature()
}

Reduce { state, action in
switch action {
case let .selectedTabChanged(tab):
if tab == .todak, state.selectedTab != .todak {
state.previousTab = state.selectedTab
}
let shouldDiscardEditPresentation = tab != .fortune
&& state.compatibilityEditSourceID != nil
state.selectedTab = tab
Expand All @@ -91,6 +106,10 @@ struct MainTabFeature {
state.selectedTab = .luckyAction
return .none

case .todak(.delegate(.closeRequested)):
state.selectedTab = state.previousTab == .todak ? .fortune : state.previousTab
return .none

case .fortune(.delegate(.luckyActionPushRequested)):
state.pushedLuckyAction = LuckyActionFeature.State(
presentationStyle: .pushed,
Expand Down Expand Up @@ -135,10 +154,11 @@ struct MainTabFeature {
action: .compatibility(.mySajuRefreshRequested)
)
)
)
)
)

case .fortune, .luckyAction, .pushedLuckyAction, .pushedLuckyActionPresentationChanged, .myPage:
case .fortune, .todak, .luckyAction, .pushedLuckyAction,
.pushedLuckyActionPresentationChanged, .myPage:
return .none
}
}
Expand Down
8 changes: 6 additions & 2 deletions Projects/App/Sources/MainTab/MainTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Foundation
import LuckyActionFeature
import MyPageFeature
import SwiftUI
import TodakFeature

struct MainTabView: View {
@Bindable var store: StoreOf<MainTabFeature>
Expand Down Expand Up @@ -38,7 +39,9 @@ struct MainTabView: View {
fortuneTab

case .todak:
UnavailableTabView(title: "토닥이")
TodakView(
store: store.scope(state: \.todak, action: \.todak)
)

case .luckyAction:
LuckyActionView(
Expand Down Expand Up @@ -83,7 +86,8 @@ struct MainTabView: View {
&& store.myPage.withdrawal == nil
&& store.myPage.sajuManagement == nil
)
return fortuneShowing
return store.selectedTab != .todak
&& fortuneShowing
&& myPageShowing
&& !isMyInfoEditPresented
&& store.pushedLuckyAction == nil
Expand Down
14 changes: 14 additions & 0 deletions Projects/App/Sources/Root/RootApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import MyPageFeature
import NetworkCore
import OnboardingFeature
import SwiftUI
import TodakFeature

@main
struct TodakunApp: App {
Expand All @@ -30,6 +31,7 @@ struct TodakunApp: App {
let fortuneClient: FortuneClient
let luckyActionClient: LuckyActionClient
let myPageClient: MyPageClient
let todakClient: TodakClient

if let baseURL = configuration.apiBaseURL {
let authHTTPClient = HTTPClient(baseURL: baseURL)
Expand All @@ -52,11 +54,22 @@ struct TodakunApp: App {
fortuneClient = FortuneClient.live(httpClient: authenticatedHTTPClient)
luckyActionClient = LuckyActionClient.live(httpClient: authenticatedHTTPClient)
myPageClient = MyPageClient.live(httpClient: authenticatedHTTPClient)
let authenticatedSSEClient = SSEClient(
baseURL: baseURL,
defaultHeaders: {
await authSession.authorizationHeaders()
}
)
todakClient = TodakClient.live(
httpClient: authenticatedHTTPClient,
sseClient: authenticatedSSEClient
)
} else {
authClient = .unavailable
fortuneClient = .unavailable
luckyActionClient = .unavailable
myPageClient = .unavailable
todakClient = .unavailable
}

Task { @MainActor in
Expand All @@ -73,6 +86,7 @@ struct TodakunApp: App {
$0.fortuneClient = fortuneClient
$0.luckyActionClient = luckyActionClient
$0.myPageClient = myPageClient
$0.todakClient = todakClient
$0.socialLoginClient = .live(configuration: configuration)
}
}
Expand Down
19 changes: 19 additions & 0 deletions Projects/App/Tests/MainTabFeatureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import FortuneFeature
import Foundation
import LuckyActionFeature
import Testing
import TodakFeature
@testable import Todakun

@Suite
Expand Down Expand Up @@ -229,4 +230,22 @@ struct MainTabFeatureTests {
store.state.fortune.viewState == .failed(message: "네트워크 오류가 발생했습니다.")
)
}

@Test
func closingTodakReturnsToPreviousTab() async {
let store = TestStore(initialState: MainTabFeature.State()) {
MainTabFeature()
}

await store.send(.selectedTabChanged(.myPage)) {
$0.selectedTab = .myPage
}
await store.send(.selectedTabChanged(.todak)) {
$0.previousTab = .myPage
$0.selectedTab = .todak
}
await store.send(.todak(.delegate(.closeRequested))) {
$0.selectedTab = .myPage
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- **긴 텍스트 처리:** 3줄 초과 시 텍스트 박스 내에서 위로 스크롤되며 가려짐. 가려지는 부분에 `#FFFFFF` 60% 레이어 씌움.
- **긴 텍스트 페이드:** 상단에 배경색 60% 불투명도 레이어를 16px 높이로 표시.
- **그림자:** black 6%, blur 20pt, y 4pt
- **커서·스크롤:** UIKit 기본 커서를 사용한다. 최대 높이 전에는 `UITextView`의 content offset을 직접 조정하지 않으며, 최대 높이 초과 시 기본 caret 스크롤을 사용한다. 입력 중에는 SwiftUI 갱신마다 `UITextView`의 font·typing attributes를 재설정하지 않는다.

## 🏗️ Structure & Layout

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@

- 하단 구분선은 셀 컴포넌트가 아닌 부모 대화 목록의 책임이다. 마지막 셀에는 구분선을 렌더링하지 않도록 목록 구현에서 제어한다.
- **대화 제목:** 고정 높이 96pt 행의 레이아웃을 유지하기 위해 한 줄로 표시하고, 초과 텍스트는 말줄임표로 처리한다.
- 제목·읽지 않음 표시 영역은 320pt, 삭제 아이콘은 23pt이며 두 영역 사이 간격은 10pt다. 삭제 아이콘과 읽지 않음 표시는 제목의 상단에 맞춘다.
- Figma 기준 화면(393pt)에서 제목·읽지 않음 표시 영역은 320pt, 삭제 아이콘은 23pt이며 두 영역 사이 간격은 10pt다. 실제 컴포넌트는 좌우 20pt inset만 고정하고 제목 영역은 남은 폭으로 확장하므로, 어떤 화면 폭에서도 삭제 아이콘은 trailing inset에 정렬된다. 삭제 아이콘과 읽지 않음 표시는 제목의 상단에 맞춘다.
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@

## 🏗️ Structure & Layout

- 🖼️ **Todak Example Question** (COMPONENT) `W: 284.0, H: 48.0`
- 🖼️ **User Chat** (INSTANCE) `W: 284.0, H: 48.0` [X: 0.0, Y: 0.0 | Fill: primary50 (#f5f3fe) (op: 1.00) | Radius: 12]
- 📝 **📅 중요한 일정 잡기 좋은 날인지 궁금해** (TEXT) `W: 248.0, H: 24.0` [X: 18.0, Y: 12.0 | Font: dsBody2Regular (Figma LH: 24.0px) | Color: coolGray800]
- 🖼️ **Todak Example Question** (COMPONENT) `H: 48.0 min`
- 🖼️ **User Chat** (INSTANCE) [Fill: primary50 (#f5f3fe) (op: 1.00) | Radius: 12]
- 📝 **📅 중요한 일정 잡기 좋은 날인지 궁금해** [X: 18.0, Y: 12.0 | Font: dsBody2Regular (Figma LH: 24.0px) | Color: coolGray800]

## 폭 계약

- Figma의 `W: 284.0`은 해당 예시 문구가 만든 결과 폭이며, 컴포넌트의 고정 폭이 아니다.
- 질문 말풍선은 텍스트의 intrinsic width와 좌우 padding `18pt`로 폭을 결정한다.
- 부모가 제공하는 너비보다 긴 문구는 부모 폭 안에서 줄바꿈하며, `48pt`는 최소 높이로 유지한다.

## 콘텐츠 API

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ struct ConversationHistoryListPlaygroundView: View {
}

Section(header: Text("Figma Specification Check")) {
DSSpecificationRow(title: "Content Width", value: specification.contentWidth.ptDescription)
DSSpecificationRow(
title: "Horizontal Padding",
value: specification.horizontalPadding.ptDescription
Expand All @@ -49,10 +48,6 @@ struct ConversationHistoryListPlaygroundView: View {
title: "Title Truncation Mode",
value: String(describing: specification.titleTruncationMode)
)
DSSpecificationRow(
title: "Title Indicator Width",
value: specification.titleIndicatorWidth.ptDescription
)
DSSpecificationRow(title: "Unread Indicator Size", value: specification.indicatorSize.ptDescription)
DSSpecificationRow(
title: "Unread Indicator Color",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ struct TodakExampleQuestionPlaygroundView: View {
.init(trailingText)
]
)
.frame(maxWidth: 329)
}

Form {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ private struct DSChatTextEditor: UIViewRepresentable {
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
textView.showsVerticalScrollIndicator = false
textView.alwaysBounceVertical = true
textView.alwaysBounceVertical = false
textView.onContentHeightChange = { measuredHeight in
guard abs(contentHeight - measuredHeight) > 0.5 else { return }
contentHeight = measuredHeight
Expand All @@ -264,8 +264,6 @@ private struct DSChatTextEditor: UIViewRepresentable {
if textView.text != text {
textView.text = text
}
configure(textView)
textView.setNeedsLayout()
}

private func configure(_ textView: DSChatTextView) {
Expand Down Expand Up @@ -293,25 +291,6 @@ private struct DSChatTextEditor: UIViewRepresentable {

func textViewDidChange(_ textView: UITextView) {
text = textView.text
textView.setNeedsLayout()

DispatchQueue.main.async {
guard let chatTextView = textView as? DSChatTextView else { return }

let textLength = (textView.text as NSString).length
let contentOverflows = chatTextView.resolvedContentHeight() > textView.bounds.height

if contentOverflows {
textView.scrollRangeToVisible(
NSRange(location: textLength, length: 0)
)
} else {
textView.setContentOffset(
CGPoint(x: textView.contentOffset.x, y: 0),
animated: false
)
}
}
}

func textViewDidBeginEditing(_ textView: UITextView) {
Expand All @@ -336,13 +315,6 @@ final class DSChatTextView: UITextView {

let contentHeight = resolvedContentHeight()

if contentHeight <= bounds.height {
setContentOffset(
CGPoint(x: contentOffset.x, y: 0),
animated: false
)
}

guard abs(lastReportedContentHeight - contentHeight) > 0.5 else { return }
lastReportedContentHeight = contentHeight
onContentHeightChange?(contentHeight)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ import SwiftUI

public struct DSConversationHistoryList: View {
public struct Specification: Sendable {
public let contentWidth: CGFloat
public let horizontalPadding: CGFloat
public let topPadding: CGFloat
public let bottomPadding: CGFloat
public let titleFont: FontStyle
public let titleColorAsset: DesignSystemColors
public let titleLineLimit: Int
public let titleTruncationMode: Text.TruncationMode
public let titleIndicatorWidth: CGFloat
public let indicatorSize: CGFloat
public let indicatorColorAsset: DesignSystemColors
public let titleIndicatorSpacing: CGFloat
Expand All @@ -24,15 +22,13 @@ public struct DSConversationHistoryList: View {
}

public static let specification = Specification(
contentWidth: 353,
horizontalPadding: 20,
topPadding: 20,
bottomPadding: 20,
titleFont: .body1Medium,
titleColorAsset: DesignSystemAsset.Colors.black,
titleLineLimit: 1,
titleTruncationMode: .tail,
titleIndicatorWidth: 320,
indicatorSize: 6,
indicatorColorAsset: DesignSystemAsset.Colors.red400,
titleIndicatorSpacing: 6,
Expand Down Expand Up @@ -84,7 +80,7 @@ public struct DSConversationHistoryList: View {
.dsDebugDetailGeometry("DSConversationHistoryList.UnreadIndicator")
}
}
.frame(width: specification.titleIndicatorWidth, alignment: .leading)
.frame(maxWidth: .infinity, alignment: .leading)

Button(action: onDelete) {
DSIcon(
Expand All @@ -103,12 +99,12 @@ public struct DSConversationHistoryList: View {
)
)
}
.frame(width: specification.contentWidth, alignment: .leading)
.frame(maxWidth: .infinity, alignment: .leading)

Text(time)
.dsFont(specification.timeFont)
.foregroundStyle(specification.timeColorAsset.swiftUIColor)
.frame(width: specification.contentWidth, alignment: .leading)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.horizontal, specification.horizontalPadding)
.padding(.top, specification.topPadding)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ public struct DSTodakExampleQuestion: View {

questionText(specification: specification)
.dsFont(specification.fontStyle)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, specification.horizontalPadding)
.padding(.vertical, specification.verticalPadding)
.frame(minHeight: specification.height)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ struct DSConversationHistoryListSpecTests {
func testSpecification() {
let specification = DSConversationHistoryList.specification

#expect(specification.contentWidth == 353)
#expect(specification.horizontalPadding == 20)
#expect(specification.topPadding == 20)
#expect(specification.bottomPadding == 20)
#expect(specification.titleFont == .body1Medium)
expectColorEqual(specification.titleColorAsset, DesignSystemAsset.Colors.black)
#expect(specification.titleLineLimit == 1)
#expect(specification.titleTruncationMode == .tail)
#expect(specification.titleIndicatorWidth == 320)
#expect(specification.indicatorSize == 6)
expectColorEqual(specification.indicatorColorAsset, DesignSystemAsset.Colors.red400)
#expect(specification.titleIndicatorSpacing == 6)
Expand Down
Loading