Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
061498f
[#75] 오늘 기록 확인을 월간 조회 API에서 전용 API로 변경
Hrepay Mar 11, 2026
a1a8863
[#75] Firebase Crashlytics dSYM 업로드 스크립트 경로 수정
Hrepay Mar 23, 2026
f66e5f0
[#75] 댓글 더보기 메뉴가 해당 버튼 위치에 표시되도록 수정
Hrepay Mar 24, 2026
5ec4081
[#75] Scheme에 OS_ACTIVITY_MODE 환경변수 추가
Hrepay Mar 24, 2026
5bf2dcf
[#75] Scheme 설정 수정 및 OS_ACTIVITY_MODE 환경변수 추가
Hrepay Mar 24, 2026
15570cd
[#75] 날짜만 있는 형식(yyyy-MM-dd) 파싱 지원 추가
Hrepay Mar 24, 2026
040fd8d
[#75] 옷장 리포트 준비중 안내 표시 및 통계 API 응답 로그 추가
Hrepay Mar 24, 2026
4e958c9
[#75] 코디 업로드 버튼 크기를 텍스트에 맞게 수정
Hrepay Mar 24, 2026
352a727
[#75] 캔버스 아이템 선택 테두리 및 핸들 기반 크기/회전 조정 구현
Hrepay Mar 24, 2026
15cdb3b
[#75] 코디 추가 페이지 이미지 영역을 정사각형으로 변경
Hrepay Mar 24, 2026
750526f
[#75] 홈 배너 탭 시 기록하기 플로우 연결
Hrepay Mar 31, 2026
d095d71
[#75] 이미지 로딩 최적화: ImageIO 다운샘플링 및 병렬 처리 적용
Hrepay Mar 31, 2026
e8addda
[#75] 크롭 뷰 최적화: 인라인 오버레이 전환 및 원본 이미지 메모리 캐시 적용
Hrepay Mar 31, 2026
1beda9f
[#75] 갤러리 선택 상태 최적화: Set 기반 분리 및 썸네일 화질 개선
Hrepay Mar 31, 2026
1b01510
[#75] 텍스트 필드 UX 개선: 클리어 버튼 및 키보드 완료 버튼 추가
Hrepay Mar 31, 2026
45fcf3d
[#75] 옷장 전체 탭 UI 수정: 뒤로가기 버튼 색상 및 검색창-카테고리 여백 조정
Hrepay Mar 31, 2026
cc4245b
[#75] 기본 프로필 이미지 고화질 버전으로 교체
Hrepay Mar 31, 2026
c99a4f0
[#75] 빈 카테고리에서 랜덤 코디 시 빈 블록 생성 버그 수정
Hrepay Mar 31, 2026
e0dbf94
[#75] 내 프로필 최애 코디 빈 상태 표시 및 옷장 이동 버튼 추가
Hrepay Mar 31, 2026
33f892a
[#75] 최애 코디 모달 개선: 태그 fallback, 탭바 딤 처리, 선택 테두리 색상 수정
Hrepay Mar 31, 2026
f040ca5
[#75] 최애 코디 더보기 페이지에서 코디 상세 모달 연결
Hrepay Mar 31, 2026
a657512
[#75] 코디보드 뒤로가기 시 카테고리 선택 뷰 상태 유지
Hrepay Mar 31, 2026
db904b5
[#75] 태그 드래그 시 이미지 영역 밖으로 나가지 않도록 제한
Hrepay Apr 1, 2026
95cbe57
[#75] 기록 완료 후 작성한 게시물 상세로 바로 이동
Hrepay Apr 1, 2026
6ffca21
[#75] AI 분석 결과 Alert 메시지 버그 수정: 배경 제거 성공 수를 S3 업로드가 아닌 실제 AI 결과 기준으로 계산
Hrepay Apr 11, 2026
b1903e1
[#75] 배경 지우개 뒤로가기 시 확인 Alert 추가
Hrepay Apr 11, 2026
135e54e
[#75] 랜덤 코디 버튼 기능 구현
Hrepay Apr 11, 2026
9cf9acf
[#75] 홈 배너 카드 전체 영역 탭 가능하도록 수정
Hrepay Apr 11, 2026
af58a6c
[#75] 새 코디 만들기 하단 옷장 카테고리 선택 및 검색 기능 연결
Hrepay Apr 13, 2026
1e8f205
[#75] 룩북 목록 및 코디 목록 화면 복귀 시 자동 새로고침
Hrepay Apr 13, 2026
b9755d5
[#75] 룩북 및 코디 다중 선택 삭제 기능 추가
Hrepay Apr 13, 2026
9bd3baf
[#75] 키보드 외부 영역 탭 시 키보드 자동 닫힘 처리
Hrepay Apr 13, 2026
9910dcb
[#75] 기록 작성 텍스트 120자 제한 및 글자 수 카운터 추가
Hrepay Apr 13, 2026
58c55b8
[#75] 홈 탭 전환 시 옷 리스트 새로고침 추가
Hrepay Apr 13, 2026
0bfde84
[#75] 팔로워/팔로잉 목록 빈 상태 및 에러 상태 문구 추가
Hrepay Apr 13, 2026
a7eb94f
[#75] 룩북명 10글자 제한 추가
Hrepay Apr 13, 2026
0b28564
[#75] 오버플로우 메뉴 아이콘 크기 통일
Hrepay Apr 13, 2026
66e194c
[#75] 기록 작성 시 날짜 선택 기능 추가
Hrepay May 2, 2026
2e549b5
[#75] 푸시 알림 기반 설정 (FCM, AppDelegate, 프로젝트 구성)
Hrepay May 2, 2026
5dad3c5
[#75] 로그인 시 프로필 캐싱 및 FCM 토큰 전송 추가
Hrepay May 2, 2026
263181d
[#75] 알림 탭 시 게시물/프로필 화면 이동 구현
Hrepay May 2, 2026
d064fe2
[#75] 설정 알림 토글 시스템 권한 연동 및 마케팅 동의 영속 저장
Hrepay May 2, 2026
c32ee01
[#75] 옷장 리포트 통계 API 및 UseCase 추가
Hrepay May 9, 2026
4e8ed92
[#75] 현재 시즌 조회 헬퍼 추가
Hrepay May 9, 2026
e8c886d
[#75] 옷장 리포트 도넛 차트 컴포넌트 및 통계 도메인 엔티티 추가
Hrepay May 9, 2026
69312b3
[#75] 옷장 리포트 막대 차트 및 카드/툴팁/바텀시트 공통 컴포넌트 추가
Hrepay May 9, 2026
96b5655
[#75] 옷장 리포트 메인/상세 페이지 및 라우팅 구현
Hrepay May 9, 2026
b4984d2
[#75] 옷 카드 브랜드/이름 미입력 시 카테고리로 fallback 처리
Hrepay May 9, 2026
0ec3d78
[#75] 옷장 활용도 체크 카드 우측 텍스트 가운데 정렬
Hrepay May 9, 2026
4772646
[#75] 옷장 리포트 상세 페이지 프리뷰 코드 Release 빌드 제외
Hrepay May 9, 2026
feea2c5
[#75] 옷장 리포트 ViewModel을 UseCase 기반으로 리팩터링
Hrepay May 17, 2026
a3a0c2c
[#75] navigationBarHidden을 toolbar(.hidden) API로 일괄 교체
Hrepay May 17, 2026
bbaca8f
[#75] os.Logger 기반 통합 로깅 도입 및 토큰 로그 마스킹
Hrepay May 17, 2026
d018fee
[#75] 마이페이지 달력에서 과거 날짜 기록 시 중복 모달이 뜨던 버그 수정
Hrepay May 17, 2026
d85455b
[#75] 빌드 설정 정리 (ATS HTTP 예외 제거, Release entitlements 분리)
Hrepay May 17, 2026
a28e306
[#75] 설정 문의하기 카카오 채널 연결 및 약관 위키 링크 chevron 표시
Hrepay May 17, 2026
bae280a
[#75] 앱 현지화를 한국어 단독으로 설정 (앱스토어 표시 언어 KO)
Hrepay May 17, 2026
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
154 changes: 154 additions & 0 deletions Codive/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//
// AppDelegate.swift
// Codive
//
// Created by Claude on 4/26/26.
//

import UIKit
import FirebaseMessaging
import UserNotifications

final class AppDelegate: NSObject, UIApplicationDelegate {

/// 앱이 죽어있을 때 푸시 탭으로 실행된 경우 저장해두는 pending 데이터
static var pendingPushUserInfo: [AnyHashable: Any]?

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
UNUserNotificationCenter.current().delegate = self
Messaging.messaging().delegate = self

requestNotificationPermission(application)

return true
}

// MARK: - APNs Token

func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
Messaging.messaging().apnsToken = deviceToken
}

func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
#if DEBUG
print("[Push] APNs 등록 실패: \(error.localizedDescription)")
#endif
}

// MARK: - Permission

private func requestNotificationPermission(_ application: UIApplication) {
let options: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(options: options) { granted, error in
#if DEBUG
if let error {
print("[Push] 권한 요청 실패: \(error.localizedDescription)")
} else {
print("[Push] 알림 권한 허용: \(granted)")
}
#endif

guard granted else { return }

DispatchQueue.main.async {
application.registerForRemoteNotifications()
}
}
}
}

// MARK: - UNUserNotificationCenterDelegate

extension AppDelegate: UNUserNotificationCenterDelegate {

// 포그라운드에서 알림 수신 시 배너 표시
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner, .badge, .sound])
}

// 알림 탭 시 처리
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo

#if DEBUG
print("[Push] 알림 탭 userInfo: \(userInfo)")
#endif

// MainTabView가 아직 없으면 pending으로 저장, 있으면 바로 전달
AppDelegate.pendingPushUserInfo = userInfo

NotificationCenter.default.post(
name: .pushNotificationTapped,
object: nil,
userInfo: userInfo
)

completionHandler()
}
}

// MARK: - MessagingDelegate

extension AppDelegate: MessagingDelegate {

func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
guard let fcmToken else { return }

#if DEBUG
AppLog.push.debug("FCM 토큰: \(fcmToken.masked(), privacy: .public)")
#endif

// UserDefaults에 저장 (로그인 후 서버 전송용)
UserDefaults.standard.set(fcmToken, forKey: "fcmToken")

// 로그인 상태면 서버에 즉시 전송
Task {
await sendFCMTokenToServerIfNeeded(fcmToken)
}
}

@MainActor
private func sendFCMTokenToServerIfNeeded(_ fcmToken: String) async {
let tokenService = TokenService()

guard tokenService.hasValidTokens(),
!tokenService.isAccessTokenExpired() else {
return
}

do {
let authAPIService = AuthAPIService()
try await authAPIService.renewDeviceToken(deviceToken: fcmToken)
#if DEBUG
print("[Push] 서버에 FCM 토큰 전송 완료")
#endif
} catch {
#if DEBUG
print("[Push] 서버 FCM 토큰 전송 실패: \(error.localizedDescription)")
#endif
}
}
}

// MARK: - Notification Name

extension Notification.Name {
static let pushNotificationTapped = Notification.Name("pushNotificationTapped")
}
3 changes: 3 additions & 0 deletions Codive/Application/AppRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ struct AppRootView: View {
.scaleEffect(1.5)
}
}
.onTapGesture {
hideKeyboard()
}
.onOpenURL { url in
handleDeepLink(url: url)
}
Expand Down
1 change: 1 addition & 0 deletions Codive/Application/CodiveApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import KakaoSDKAuth
@main
struct CodiveApp: App {

@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let appDIContainer = AppDIContainer()

init() {
Expand Down
14 changes: 14 additions & 0 deletions Codive/Codive.Release.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
<key>com.apple.developer.weatherkit</key>
<true/>
<key>aps-environment</key>
<string>production</string>
</dict>
</plist>
2 changes: 2 additions & 0 deletions Codive/Codive.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@
</array>
<key>com.apple.developer.weatherkit</key>
<true/>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>
32 changes: 32 additions & 0 deletions Codive/Core/Utils/Logger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// Logger.swift
// Codive
//
// os.Logger 기반 통합 로깅 래퍼.
// Release 빌드에서는 .debug 레벨이 자동 무시되며,
// 민감 정보(토큰 등)는 `masked()` 헬퍼로 마스킹하여 기록한다.
//

import Foundation
import OSLog

enum AppLog {
private static let subsystem = "com.codive.app"

static let app = Logger(subsystem: subsystem, category: "app")
static let auth = Logger(subsystem: subsystem, category: "auth")
static let network = Logger(subsystem: subsystem, category: "network")
static let api = Logger(subsystem: subsystem, category: "api")
static let push = Logger(subsystem: subsystem, category: "push")
static let deeplink = Logger(subsystem: subsystem, category: "deeplink")
static let ui = Logger(subsystem: subsystem, category: "ui")
}

extension String {
/// 민감 문자열의 앞 일부만 노출하고 나머지는 마스킹.
/// - Parameter visible: 앞에서 그대로 보여줄 문자 수 (기본 8)
func masked(visible: Int = 8) -> String {
guard count > visible else { return "***" }
return prefix(visible) + "...(\(count))"
}
}
12 changes: 8 additions & 4 deletions Codive/DIContainer/AddDIContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ final class AddDIContainer {
let navigationRouter: NavigationRouter
lazy var addViewFactory = AddViewFactory(addDIContainer: self)

// 캘린더에서 선택한 날짜 (기록 생성 시 사용)
var selectedDate: Date?

// 지우개 편집 시 공유 참조
weak var activeClothAddViewModel: ClothAddViewModel?
var pendingErasedImage: UIImage?
Expand Down Expand Up @@ -46,10 +49,11 @@ final class AddDIContainer {
)
}

func makeRecordDetailViewModel(selectedPhotos: [SelectedPhoto]) -> RecordDetailViewModel {
func makeRecordDetailViewModel(selectedPhotos: [SelectedPhoto], selectedDate: Date? = nil) -> RecordDetailViewModel {
return RecordDetailViewModel(
selectedPhotos: selectedPhotos,
navigationRouter: navigationRouter
navigationRouter: navigationRouter,
selectedDate: selectedDate
)
}

Expand Down Expand Up @@ -93,9 +97,9 @@ final class AddDIContainer {
return RecordAddView(viewModel: makeRecordAddViewModel(flowType: flowType))
}

func makeRecordDetailView(selectedPhotos: [SelectedPhoto]) -> RecordDetailView {
func makeRecordDetailView(selectedPhotos: [SelectedPhoto], selectedDate: Date? = nil) -> RecordDetailView {
return RecordDetailView(
viewModel: makeRecordDetailViewModel(selectedPhotos: selectedPhotos)
viewModel: makeRecordDetailViewModel(selectedPhotos: selectedPhotos, selectedDate: selectedDate)
)
}

Expand Down
3 changes: 2 additions & 1 deletion Codive/DIContainer/AuthDIContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ final class AuthDIContainer {
return OnboardingViewModel(
appRouter: appRouter,
navigationRouter: navigationRouter,
authRepository: authRepository
authRepository: authRepository,
authAPIService: authAPIService
)
}

Expand Down
63 changes: 61 additions & 2 deletions Codive/DIContainer/ClosetDIContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,26 @@ final class ClosetDIContainer {
return CheckStatisticsConditionUseCase(repository: statisticsRepository)
}

func makeFetchFavoriteItemsUseCase() -> FetchFavoriteItemsUseCase {
return FetchFavoriteItemsUseCase(repository: statisticsRepository)
}

func makeFetchFavoriteCategoryItemsUseCase() -> FetchFavoriteCategoryItemsUseCase {
return FetchFavoriteCategoryItemsUseCase(repository: statisticsRepository)
}

func makeFetchClosetUtilizationUseCase() -> FetchClosetUtilizationUseCase {
return FetchClosetUtilizationUseCase(repository: statisticsRepository)
}

func makeFetchClothListByCategoryUseCase() -> FetchClothListByCategoryUseCase {
return FetchClothListByCategoryUseCase(repository: clothRepository)
}

func makeFetchClothDetailUseCase() -> FetchClothDetailUseCase {
return FetchClothDetailUseCase(repository: clothRepository)
}

// MARK: - ViewModels
func makeMyClosetViewModel() -> MyClosetViewModel {
return MyClosetViewModel(
Expand Down Expand Up @@ -108,7 +128,7 @@ final class ClosetDIContainer {
cloth: cloth,
navigationRouter: navigationRouter,
deleteClothItemsUseCase: makeDeleteClothItemsUseCase(),
clothRepository: clothRepository
fetchClothDetailUseCase: makeFetchClothDetailUseCase()
)
}

Expand All @@ -123,7 +143,34 @@ final class ClosetDIContainer {
func makeWardrobeReportDetailViewModel() -> WardrobeReportDetailViewModel {
return WardrobeReportDetailViewModel(
navigationRouter: navigationRouter,
checkStatisticsConditionUseCase: makeCheckStatisticsConditionUseCase()
checkStatisticsConditionUseCase: makeCheckStatisticsConditionUseCase(),
fetchFavoriteItemsUseCase: makeFetchFavoriteItemsUseCase(),
fetchFavoriteCategoryItemsUseCase: makeFetchFavoriteCategoryItemsUseCase(),
fetchClosetUtilizationUseCase: makeFetchClosetUtilizationUseCase()
)
}

func makeFavoriteByCategoryViewModel(parentCategoryId: Int64) -> FavoriteByCategoryViewModel {
return FavoriteByCategoryViewModel(
navigationRouter: navigationRouter,
fetchFavoriteCategoryItemsUseCase: makeFetchFavoriteCategoryItemsUseCase(),
fetchClothListByCategoryUseCase: makeFetchClothListByCategoryUseCase(),
parentCategoryId: parentCategoryId
)
}

func makeItemDataViewModel() -> ItemDataViewModel {
return ItemDataViewModel(
navigationRouter: navigationRouter,
fetchFavoriteItemsUseCase: makeFetchFavoriteItemsUseCase(),
fetchClothListByCategoryUseCase: makeFetchClothListByCategoryUseCase()
)
}

func makeWearingDataViewModel() -> WearingDataViewModel {
return WearingDataViewModel(
navigationRouter: navigationRouter,
fetchClosetUtilizationUseCase: makeFetchClosetUtilizationUseCase()
)
}

Expand All @@ -143,4 +190,16 @@ final class ClosetDIContainer {
func makeWardrobeReportDetailView() -> some View {
return WardrobeReportDetailView(viewModel: makeWardrobeReportDetailViewModel())
}

func makeFavoriteByCategoryView(parentCategoryId: Int64) -> some View {
return FavoriteByCategoryView(viewModel: makeFavoriteByCategoryViewModel(parentCategoryId: parentCategoryId))
}

func makeItemDataView() -> some View {
return ItemDataView(viewModel: makeItemDataViewModel())
}

func makeWearingDataView() -> some View {
return WearingDataView(viewModel: makeWearingDataViewModel())
}
}
10 changes: 6 additions & 4 deletions Codive/Features/Auth/Presentation/View/TermsAgreementView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,15 @@ struct TermsAgreementView: View {

// 개별 항목들
ForEach(termsList, id: \.termId) { term in
let wikiURL = termsWikiURL(for: term.title)
AgreementRow(
title: term.title,
isAgreed: binding(for: term.termId),
isRequired: !term.isOptional
isRequired: !term.isOptional,
showChevron: wikiURL != nil
) {
if let url = termsWikiURL(for: term.title) {
selectedTermsURL = TermsURL(url: url)
if let wikiURL {
selectedTermsURL = TermsURL(url: wikiURL)
}
}
}
Expand Down Expand Up @@ -124,7 +126,7 @@ struct TermsAgreementView: View {
.padding(.horizontal, 20)
.padding(.bottom, 10)
}
.navigationBarHidden(true)
.toolbar(.hidden, for: .navigationBar)
.task {
await loadTerms()
}
Expand Down
Loading