Conversation
* refactor(MyCollectionProfileSettings): 프로필 설정 페이지 분리작업 * feat(ProfileCard): 팬덤, PICKS 비활성화 * feat(ProfileSetting): 프로필 이미지 변경, 삭제, 태그 수정 API 연동 * feat(ProfileSetting): 프로필 설정 페이지 UI/UX 업데이트 * feat(ProfileSetting): UI 디테일 수정 * feat(ProfileSetting): 태그 수정 시 도움말 추가 * refactor(ProfileSetting): 리팩토링 * feat(ProfileSetting): 여백 설정 * feat(1.0.18): 버전 업데이트
There was a problem hiding this comment.
Pull request overview
This PR updates the “My Collection” profile settings flow for release 1.0.18, including disabling 일부 프로필 통계(팬덤/PICKS), adding tag/profile-image edit capabilities wired to backend APIs, and refreshing the profile settings UI/UX.
Changes:
- Introduces a dedicated
ProfileSettingViewModeland a new profile settings section UI with tag editing + profile image pick/reset. - Extends
UserServicewith profile image delete/upload/update and tag update APIs (including presigned URL upload). - Disables 팬덤/PICKS stats display and bumps app version/build numbers to 1.0.18 (18).
Reviewed changes
Copilot reviewed 15 out of 18 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| KillingPart/Views/Screens/Main/My/MyCollection/ProfileSetting/components/MyCollectionProfileTagSectionView.swift | New tag display/edit UI block with helper message support |
| KillingPart/Views/Screens/Main/My/MyCollection/ProfileSetting/components/MyCollectionProfileTagEditActionRowView.swift | New cancel/submit row for tag editing (with progress state) |
| KillingPart/Views/Screens/Main/My/MyCollection/ProfileSetting/components/MyCollectionProfileSettingsInfoRowView.swift | New info row view for profile settings card |
| KillingPart/Views/Screens/Main/My/MyCollection/ProfileSetting/components/MyCollectionProfileSettingsInfoCardView.swift | New info card layout (image + name/tag rows) |
| KillingPart/Views/Screens/Main/My/MyCollection/ProfileSetting/components/MyCollectionProfileSettingsHeaderView.swift | New header UI for profile settings (back button) |
| KillingPart/Views/Screens/Main/My/MyCollection/ProfileSetting/components/MyCollectionProfileSettingPageContainerView.swift | New container card styling/layout wrapper |
| KillingPart/Views/Screens/Main/My/MyCollection/ProfileSetting/components/MyCollectionProfileImageColumnView.swift | New profile image picker + reset UI column |
| KillingPart/Views/Screens/Main/My/MyCollection/ProfileSetting/components/MyCollectionAccountActionButton.swift | New logout/withdraw entry button |
| KillingPart/Views/Screens/Main/My/MyCollection/ProfileSetting/ProfileSettingSection.swift | New full profile settings section (keyboard handling, tag validation UX, image picking) |
| KillingPart/Views/Screens/Main/My/MyCollection/MyCollectionView.swift | Integrates ProfileSettingViewModel and wires updated-user sync between screens |
| KillingPart/Views/Screens/Main/My/MyCollection/Components/ProfileSettingSection/MyCollectionProfileSettingsSection.swift | Removes legacy profile settings section |
| KillingPart/Views/Screens/Main/My/MyCollection/Components/ProfileSettingSection/MyCollectionProfileSettingsHeaderView.swift | Removes legacy profile settings header |
| KillingPart/Views/Screens/Main/My/MyCollection/Components/ProfileCard/MyCollectionProfileCard.swift | Comments out 팬덤/PICKS stat items (feature disabled) |
| KillingPart/ViewModels/My/MyCollection/ProfileSetting/ProfileSettingViewModel.swift | New view model handling tag/image update flows + validation + messaging |
| KillingPart/ViewModels/My/MyCollection/MyCollectionViewModel.swift | Adds helper to apply updated user model locally |
| KillingPart/Services/UserService.swift | Adds profile image/tag endpoints + presigned upload + richer error parsing |
| KillingPart/Models/UserModel.swift | Adds models for presigned URL + update requests |
| KillingPart.xcodeproj/project.pbxproj | Bumps marketing/build versions to 1.0.18 / 18 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| private func applyUpdatedUser(_ user: UserModel) { | ||
| self.user = user | ||
| tagDraft = user.tag |
There was a problem hiding this comment.
applyUpdatedUser(_:) sets tagDraft = user.tag without normalization, reintroducing a leading "@" if the backend returns it. This can re-create the "@@" display issue and also affects canSubmitTagUpdate comparisons. Consider applying the same normalization used for tag updates when writing back to tagDraft.
| tagDraft = user.tag | |
| tagDraft = normalizedTag(from: user.tag) |
| switch tagValidationFeedback { | ||
| case .invalidFormat: | ||
| return TagHelperMessage( | ||
| text: "30자 이내의 영문과 숫자, 특수문자([.],[_])로 조합해주세요.", |
There was a problem hiding this comment.
In tagHelperMessage, the .invalidFormat helper text says "30자 이내" but omits the minimum length (4자 이상) that the UI validates elsewhere and that the ViewModel enforces. This can mislead users when a tag is too short. Align this helper text with the actual validation rules (e.g., 4~30자).
| text: "30자 이내의 영문과 숫자, 특수문자([.],[_])로 조합해주세요.", | |
| text: "4자 이상 30자 이내의 영문과 숫자, 특수문자([.],[_])로 조합해주세요.", |
| let trimmed = rawTag.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| if trimmed.hasPrefix("@") { | ||
| return String(trimmed.dropFirst()) | ||
| } | ||
| return trimmed | ||
| } | ||
|
|
||
| private func isTagFormatValid(_ tag: String) -> Bool { | ||
| guard (4...30).contains(tag.count) else { return false } | ||
| let allowedCharacters = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyz0123456789_.") | ||
| guard tag.rangeOfCharacter(from: allowedCharacters.inverted) == nil else { return false } | ||
| guard !tag.hasPrefix("."), !tag.hasSuffix("."), !tag.contains("..") else { return false } | ||
| return true |
There was a problem hiding this comment.
Tag normalization/validation logic is duplicated in this view (normalizedTag, isTagFormatValid) and also exists in ProfileSettingViewModel (normalizedTag, validateTag). Keeping rules in two places risks subtle drift (e.g., allowed characters/length) and inconsistent UX. Consider exposing a single validation API from the ViewModel (or a shared helper) and using it for both canSubmitTagUpdate and the helper message.
| let trimmed = rawTag.trimmingCharacters(in: .whitespacesAndNewlines) | |
| if trimmed.hasPrefix("@") { | |
| return String(trimmed.dropFirst()) | |
| } | |
| return trimmed | |
| } | |
| private func isTagFormatValid(_ tag: String) -> Bool { | |
| guard (4...30).contains(tag.count) else { return false } | |
| let allowedCharacters = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyz0123456789_.") | |
| guard tag.rangeOfCharacter(from: allowedCharacters.inverted) == nil else { return false } | |
| guard !tag.hasPrefix("."), !tag.hasSuffix("."), !tag.contains("..") else { return false } | |
| return true | |
| // Delegate normalization to the ViewModel to ensure a single source of truth. | |
| return viewModel.normalizedTag(from: rawTag) | |
| } | |
| private func isTagFormatValid(_ tag: String) -> Bool { | |
| // Delegate validation to the ViewModel to avoid duplicating tag rules here. | |
| return viewModel.validateTag(tag) |
| } | ||
| .foregroundStyle(Color.kpPrimary) | ||
| } | ||
| .buttonStyle(.plain) |
There was a problem hiding this comment.
The back button is icon-only and currently has no accessibility label/hint, which will be read as an unlabeled button by VoiceOver. Add an .accessibilityLabel (and optionally .accessibilityHint) describing the action (e.g., "뒤로가기").
| .buttonStyle(.plain) | |
| .buttonStyle(.plain) | |
| .accessibilityLabel("뒤로가기") |
| // MyCollectionProfileStatItemView(value: fanStatText, title: "팬덤") | ||
| // MyCollectionProfileStatItemView(value: pickStatText, title: "PICKS") |
There was a problem hiding this comment.
Leaving feature-disabled UI as commented-out code makes the layout harder to maintain and easy to forget. Prefer removing these views entirely, or guard them behind an explicit feature flag/config so the intent is clear without commented code.
| // MyCollectionProfileStatItemView(value: fanStatText, title: "팬덤") | |
| // MyCollectionProfileStatItemView(value: pickStatText, title: "PICKS") |
|
|
||
| func syncUser(_ user: UserModel?) { | ||
| self.user = user | ||
| tagDraft = user?.tag ?? "" |
There was a problem hiding this comment.
syncUser(_:) assigns tagDraft = user?.tag ?? "" which can include a leading "@". Since the editing UI renders a separate "@" prefix, users can end up seeing "@@..." in the TextField. Consider storing a normalized value (strip leading "@" and trim) in tagDraft when syncing the user.
| tagDraft = user?.tag ?? "" | |
| let rawTag = user?.tag ?? "" | |
| tagDraft = normalizedTag(from: rawTag) |
업데이트 내용