|
| 1 | +// |
| 2 | +// MyCertificationItem.swift |
| 3 | +// CERTI-iOS |
| 4 | +// |
| 5 | +// Created by OneTen on 11/23/25. |
| 6 | +// |
| 7 | + |
| 8 | +import SwiftUI |
| 9 | + |
| 10 | +struct MyCertificationItem: View { |
| 11 | + |
| 12 | + enum CertificationType { |
| 13 | + case completed(date: String, score: String?) |
| 14 | + case expected(location: String, time: String) |
| 15 | + |
| 16 | + var text: String { |
| 17 | + switch self { |
| 18 | + case .completed: return "취득 완료" |
| 19 | + case .expected: return "취득 예정" |
| 20 | + } |
| 21 | + } |
| 22 | + |
| 23 | + var color: Color { |
| 24 | + switch self { |
| 25 | + case .completed: return .mainblue |
| 26 | + case .expected: return .purpleblue |
| 27 | + } |
| 28 | + } |
| 29 | + } |
| 30 | + |
| 31 | + enum EditType { |
| 32 | + case viewOnly // 수정, 삭제 ㄴㄴ |
| 33 | + case editable(onEdit: () -> Void, onDelete: () -> Void) // 수정, 삭제 ㅇㅇ |
| 34 | + } |
| 35 | + |
| 36 | + |
| 37 | + // MARK: - Properties |
| 38 | + |
| 39 | + let type: CertificationType |
| 40 | + let title: String |
| 41 | + let category: String |
| 42 | + let description: String |
| 43 | + let actionConfig: EditType |
| 44 | + |
| 45 | + |
| 46 | + // MARK: - Main Body |
| 47 | + |
| 48 | + var body: some View { |
| 49 | + VStack(alignment: .leading, spacing: 0) { |
| 50 | + |
| 51 | + topBadge |
| 52 | + .padding(.bottom, 8) |
| 53 | + |
| 54 | + titleSection |
| 55 | + .padding(.bottom, 4) |
| 56 | + |
| 57 | + Text(description) |
| 58 | + .applyCertiFont(.caption_regular_12) |
| 59 | + .foregroundStyle(.grayscale500) |
| 60 | + .frame(height: 18) |
| 61 | + .padding(.bottom, 12) |
| 62 | + |
| 63 | + infoSection |
| 64 | + |
| 65 | + } |
| 66 | + .padding(16) |
| 67 | + .background(.white) |
| 68 | + .clipShape(RoundedRectangle(cornerRadius: 12)) |
| 69 | + .overlay( |
| 70 | + RoundedRectangle(cornerRadius: 12) |
| 71 | + .stroke(.lightpurple, lineWidth: 1) |
| 72 | + ) |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | + |
| 77 | +// MARK: - Subviews |
| 78 | + |
| 79 | +extension MyCertificationItem { |
| 80 | + @ViewBuilder |
| 81 | + private var topBadge: some View { |
| 82 | + HStack(alignment: .center, spacing: 0) { |
| 83 | + HStack(alignment: .center, spacing: 4) { |
| 84 | + Image(systemName: "checkmark.circle.fill") |
| 85 | + .resizable() |
| 86 | + .scaledToFit() |
| 87 | + .frame(width: 10, height: 10) |
| 88 | + |
| 89 | + Text(type.text) |
| 90 | + .applyCertiFont(.caption_semibold_10) |
| 91 | + } |
| 92 | + .foregroundStyle(.white) |
| 93 | + .padding(6) |
| 94 | + .background(type.color) |
| 95 | + .clipShape(Capsule()) |
| 96 | + |
| 97 | + Spacer() |
| 98 | + |
| 99 | + switch actionConfig { |
| 100 | + case .viewOnly: |
| 101 | + EmptyView() |
| 102 | + case .editable(let onEdit, let onDelete): |
| 103 | + HStack(alignment: .center, spacing: 12) { |
| 104 | + actionButton(title: "수정", action: onEdit) |
| 105 | + actionButton(title: "삭제", action: onDelete) |
| 106 | + } |
| 107 | + } |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + @ViewBuilder |
| 112 | + private var titleSection: some View { |
| 113 | + HStack(alignment: .center, spacing: 8) { |
| 114 | + Text(title) |
| 115 | + .applyCertiFont(.sub_semibold_20) |
| 116 | + |
| 117 | + Text(category) |
| 118 | + .applyCertiFont(.caption_regular_12) |
| 119 | + } |
| 120 | + .foregroundStyle(.black) |
| 121 | + .frame(height: 26) |
| 122 | + } |
| 123 | + |
| 124 | + @ViewBuilder |
| 125 | + private var infoSection: some View { |
| 126 | + HStack(alignment: .center, spacing: 8) { |
| 127 | + switch type { |
| 128 | + case .expected(let location, let time): |
| 129 | + // [취득 예정] 장소 + 시간 |
| 130 | + infoItem(icon: .iconPlacemark16, text: location) |
| 131 | + infoItem(icon: .iconTime16, text: time) |
| 132 | + |
| 133 | + case .completed(let date, let score): |
| 134 | + // [취득 완료] 날짜 + 점수 |
| 135 | + infoItem(icon: .iconDate16, text: date) |
| 136 | + infoItem(icon: .iconLevel16, text: score ?? "취득 점수 -") |
| 137 | + } |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + @ViewBuilder |
| 142 | + private func infoItem(icon: UIImage, text: String) -> some View { |
| 143 | + HStack(spacing: 4) { |
| 144 | + Image(uiImage: icon) |
| 145 | + .foregroundStyle(.grayscale300) |
| 146 | + |
| 147 | + Text(text) |
| 148 | + .applyCertiFont(.caption_regular_14) |
| 149 | + .foregroundStyle(text == "취득 점수 -" ? .grayscale200 : .black) |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + @ViewBuilder |
| 154 | + private func actionButton(title: String, action: @escaping () -> Void) -> some View { |
| 155 | + Button { |
| 156 | + action() |
| 157 | + } label: { |
| 158 | + Text(title) |
| 159 | + .applyCertiFont(.caption_regular_12) |
| 160 | + .foregroundStyle(.grayscale600) |
| 161 | + .padding(.horizontal, 12) |
| 162 | + .padding(.vertical, 4) |
| 163 | + .background(.white) |
| 164 | + .overlay( |
| 165 | + Capsule() |
| 166 | + .stroke(.grayscale300, lineWidth: 1) |
| 167 | + ) |
| 168 | + } |
| 169 | + } |
| 170 | +} |
| 171 | + |
| 172 | +#Preview { |
| 173 | + VStack(spacing: 20) { |
| 174 | + |
| 175 | + // case 1: 취득 예정, 버튼 없음 |
| 176 | + MyCertificationItem( |
| 177 | + type: .expected(location: "고양시", time: "09:00"), |
| 178 | + title: "정보처리기사", |
| 179 | + category: "국가기술자격", |
| 180 | + description: "소프트웨어 개발 관련 자격증...", |
| 181 | + actionConfig: .viewOnly |
| 182 | + ) |
| 183 | + |
| 184 | + // case 2: 취득 예정, 버튼 있음 |
| 185 | + MyCertificationItem( |
| 186 | + type: .expected(location: "서울시", time: "14:00"), |
| 187 | + title: "정보보안기사", |
| 188 | + category: "국가기술자격", |
| 189 | + description: "보안 관련 자격증...", |
| 190 | + actionConfig: .editable(onEdit: { |
| 191 | + print("수정") |
| 192 | + }, onDelete: { |
| 193 | + print("삭제") |
| 194 | + }) |
| 195 | + ) |
| 196 | + |
| 197 | + // case 3: 취득 완료, 버튼 없음, 점수 있음 |
| 198 | + MyCertificationItem( |
| 199 | + type: .completed(date: "2023. 11. 23", score: "IM3"), |
| 200 | + title: "OPIC", |
| 201 | + category: "어학", |
| 202 | + description: "영어 말하기 시험...", |
| 203 | + actionConfig: .viewOnly |
| 204 | + ) |
| 205 | + |
| 206 | + // case 4: 취득 완료, 버튼 있음, 점수 없음 |
| 207 | + MyCertificationItem( |
| 208 | + type: .completed(date: "2023. 11. 23", score: nil), |
| 209 | + title: "OPIC", |
| 210 | + category: "어학", |
| 211 | + description: "영어 말하기 시험...", |
| 212 | + actionConfig: .editable(onEdit: { |
| 213 | + print("수정") |
| 214 | + }, onDelete: { |
| 215 | + print("삭제") |
| 216 | + }) |
| 217 | + ) |
| 218 | + } |
| 219 | + .padding() |
| 220 | + .background(Color.gray.opacity(0.1)) |
| 221 | +} |
0 commit comments