|
| 1 | +// |
| 2 | +// NicknameValidator.swift |
| 3 | +// EATSSU |
| 4 | +// |
| 5 | +// Created by ν©μν on 10/24/25. |
| 6 | +// |
| 7 | + |
| 8 | +import Foundation |
| 9 | + |
| 10 | +final class NicknameValidator { |
| 11 | + |
| 12 | + // MARK: - Validation Methods |
| 13 | + |
| 14 | + /// λλ€μ μ ν¨μ± κ²μ¬ (λͺ¨λ κ·μΉ 체ν¬) |
| 15 | + static func validate(_ nickname: String) -> NicknameTextFieldResultType? { |
| 16 | + // λΉ λ¬Έμμ΄ μ²΄ν¬ |
| 17 | + if nickname.isEmpty { |
| 18 | + return .textFieldEmpty |
| 19 | + } |
| 20 | + |
| 21 | + // κΈΈμ΄ μ²΄ν¬ (2~16μ) |
| 22 | + if !(2...16).contains(nickname.count) { |
| 23 | + return .invalidLength |
| 24 | + } |
| 25 | + |
| 26 | + // λμ΄μ°κΈ° μμ/λ μ²΄ν¬ |
| 27 | + if hasWhitespaceAtStartOrEnd(nickname) { |
| 28 | + return .whitespaceAtStartOrEnd |
| 29 | + } |
| 30 | + |
| 31 | + // μ°μ λμ΄μ°κΈ° μ²΄ν¬ |
| 32 | + if hasConsecutiveWhitespace(nickname) { |
| 33 | + return .consecutiveWhitespace |
| 34 | + } |
| 35 | + |
| 36 | + // μ΄λͺ¨μ§ λ° νμ©λμ§ μμ νΉμλ¬Έμ μ²΄ν¬ |
| 37 | + if containsEmojiOrInvalidSpecialChar(nickname) { |
| 38 | + return .emojiOrSpecialChar |
| 39 | + } |
| 40 | + |
| 41 | + // νμ©λμ§ μμ λ¬Έμ μ²΄ν¬ |
| 42 | + if !isAllowedCharacters(nickname) { |
| 43 | + return .invalidCharacters |
| 44 | + } |
| 45 | + |
| 46 | + // μμκ³Ό λμ΄ νκΈ(μ΄μ±ν¬ν¨), μλ¬Έ, μ«μμΈμ§ μ²΄ν¬ |
| 47 | + if !isValidStartAndEnd(nickname) { |
| 48 | + return .invalidStartOrEnd |
| 49 | + } |
| 50 | + |
| 51 | + // νΉμλ¬Έμ μ°μ μ¬μ© μ²΄ν¬ |
| 52 | + if hasConsecutiveSpecialChars(nickname) { |
| 53 | + return .consecutiveSpecialChars |
| 54 | + } |
| 55 | + |
| 56 | + // μ«μλ‘λ§ κ΅¬μ±λμλμ§ μ²΄ν¬ |
| 57 | + if isOnlyNumbers(nickname) { |
| 58 | + return .onlyNumbers |
| 59 | + } |
| 60 | + |
| 61 | + // κΈμ§μ΄ μ²΄ν¬ (νμ
λ³) |
| 62 | + if let bannedWordType = checkBannedWordType(nickname) { |
| 63 | + return bannedWordType |
| 64 | + } |
| 65 | + |
| 66 | + // λͺ¨λ κ²μ¬ ν΅κ³Ό - μ€λ³΅ νμΈ νμ |
| 67 | + return .nicknameTextFieldDoubleCheck |
| 68 | + } |
| 69 | + |
| 70 | + // MARK: - Private Helper Methods |
| 71 | + |
| 72 | + /// λμ΄μ°κΈ°λ‘ μμνκ±°λ λλλμ§ μ²΄ν¬ |
| 73 | + private static func hasWhitespaceAtStartOrEnd(_ nickname: String) -> Bool { |
| 74 | + return nickname.first == " " || nickname.last == " " |
| 75 | + } |
| 76 | + |
| 77 | + /// μ°μλ λμ΄μ°κΈ°κ° μλμ§ μ²΄ν¬ |
| 78 | + private static func hasConsecutiveWhitespace(_ nickname: String) -> Bool { |
| 79 | + return nickname.contains(" ") |
| 80 | + } |
| 81 | + |
| 82 | + /// μ΄λͺ¨μ§ λλ νμ©λμ§ μμ νΉμλ¬Έμ ν¬ν¨ μ¬λΆ μ²΄ν¬ |
| 83 | + private static func containsEmojiOrInvalidSpecialChar(_ nickname: String) -> Bool { |
| 84 | + for scalar in nickname.unicodeScalars { |
| 85 | + // μ΄λͺ¨μ§ μ²΄ν¬ |
| 86 | + if scalar.properties.isEmoji { |
| 87 | + return true |
| 88 | + } |
| 89 | + |
| 90 | + // νμ©λ λ¬Έμ μ§ν©μ μλ νΉμλ¬Έμ μ²΄ν¬ |
| 91 | + let char = Character(scalar) |
| 92 | + if char.isPunctuation || char.isSymbol { |
| 93 | + // - μ _ λ νμ© |
| 94 | + if char != "-" && char != "_" { |
| 95 | + return true |
| 96 | + } |
| 97 | + } |
| 98 | + } |
| 99 | + return false |
| 100 | + } |
| 101 | + |
| 102 | + /// νμ©λ λ¬Έμλ§ ν¬ν¨λμ΄ μλμ§ μ²΄ν¬ (νκΈ(μ΄μ±ν¬ν¨), μλ¬Έ, μ«μ, λμ΄μ°κΈ°, -, _) |
| 103 | + private static func isAllowedCharacters(_ nickname: String) -> Bool { |
| 104 | + var allowed = CharacterSet() |
| 105 | + |
| 106 | + // μλ¬Έ μλ¬Έμ |
| 107 | + allowed.formUnion(CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyz")) |
| 108 | + // μλ¬Έ λλ¬Έμ |
| 109 | + allowed.formUnion(CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZ")) |
| 110 | + // μ«μ |
| 111 | + allowed.formUnion(CharacterSet.decimalDigits) |
| 112 | + // νκΈ (μμ±λ κΈμ) |
| 113 | + allowed.formUnion(CharacterSet(charactersIn: "κ°".unicodeScalars.first!..."ν£".unicodeScalars.first!)) |
| 114 | + // νκΈ μμ |
| 115 | + allowed.formUnion(CharacterSet(charactersIn: "γ±".unicodeScalars.first!..."γ
".unicodeScalars.first!)) |
| 116 | + // νκΈ λͺ¨μ |
| 117 | + allowed.formUnion(CharacterSet(charactersIn: "γ
".unicodeScalars.first!..."γ
£".unicodeScalars.first!)) |
| 118 | + // νΉμλ¬Έμ (- μ _ λ§ νμ©) |
| 119 | + allowed.formUnion(CharacterSet(charactersIn: "-_")) |
| 120 | + // λμ΄μ°κΈ° νμ© |
| 121 | + allowed.formUnion(CharacterSet(charactersIn: " ")) |
| 122 | + |
| 123 | + return nickname.unicodeScalars.allSatisfy { allowed.contains($0) } |
| 124 | + } |
| 125 | + |
| 126 | + /// μμκ³Ό λμ΄ νκΈ(μ΄μ±ν¬ν¨), μλ¬Έ, μ«μμΈμ§ μ²΄ν¬ |
| 127 | + private static func isValidStartAndEnd(_ nickname: String) -> Bool { |
| 128 | + guard let first = nickname.first, let last = nickname.last else { |
| 129 | + return false |
| 130 | + } |
| 131 | + |
| 132 | + var valid = CharacterSet() |
| 133 | + |
| 134 | + // μλ¬Έ μλ¬Έμ |
| 135 | + valid.formUnion(CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyz")) |
| 136 | + // μλ¬Έ λλ¬Έμ |
| 137 | + valid.formUnion(CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZ")) |
| 138 | + // μ«μ |
| 139 | + valid.formUnion(CharacterSet.decimalDigits) |
| 140 | + // νκΈ (μμ±λ κΈμ) |
| 141 | + valid.formUnion(CharacterSet(charactersIn: "κ°".unicodeScalars.first!..."ν£".unicodeScalars.first!)) |
| 142 | + // νκΈ μμ |
| 143 | + valid.formUnion(CharacterSet(charactersIn: "γ±".unicodeScalars.first!..."γ
".unicodeScalars.first!)) |
| 144 | + // νκΈ λͺ¨μ |
| 145 | + valid.formUnion(CharacterSet(charactersIn: "γ
".unicodeScalars.first!..."γ
£".unicodeScalars.first!)) |
| 146 | + |
| 147 | + let firstString = String(first) |
| 148 | + let lastString = String(last) |
| 149 | + |
| 150 | + let isFirstValid = firstString.unicodeScalars.allSatisfy { valid.contains($0) } |
| 151 | + let isLastValid = lastString.unicodeScalars.allSatisfy { valid.contains($0) } |
| 152 | + |
| 153 | + return isFirstValid && isLastValid |
| 154 | + } |
| 155 | + |
| 156 | + /// νΉμλ¬Έμ(-, _)κ° μ°μμΌλ‘ μ¬μ©λμλμ§ μ²΄ν¬ |
| 157 | + private static func hasConsecutiveSpecialChars(_ nickname: String) -> Bool { |
| 158 | + let specialChars: [Character] = ["-", "_"] |
| 159 | + |
| 160 | + for i in 0..<nickname.count - 1 { |
| 161 | + let currentIndex = nickname.index(nickname.startIndex, offsetBy: i) |
| 162 | + let nextIndex = nickname.index(nickname.startIndex, offsetBy: i + 1) |
| 163 | + |
| 164 | + let currentChar = nickname[currentIndex] |
| 165 | + let nextChar = nickname[nextIndex] |
| 166 | + |
| 167 | + if specialChars.contains(currentChar) && specialChars.contains(nextChar) { |
| 168 | + return true |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | + return false |
| 173 | + } |
| 174 | + |
| 175 | + /// μ«μλ‘λ§ κ΅¬μ±λμ΄ μλμ§ μ²΄ν¬ |
| 176 | + private static func isOnlyNumbers(_ nickname: String) -> Bool { |
| 177 | + return nickname.allSatisfy { $0.isNumber } |
| 178 | + } |
| 179 | + |
| 180 | + /// κΈμ§μ΄ νμ
λ³ μ²΄ν¬ |
| 181 | + private static func checkBannedWordType(_ nickname: String) -> NicknameTextFieldResultType? { |
| 182 | + // κ΄λ¦¬μ/μ΄μμ κ΄λ ¨ κΈμ§μ΄ μ²΄ν¬ |
| 183 | + if NicknameBannedWords.containsAdminWord(nickname) { |
| 184 | + return .adminRelatedWord |
| 185 | + } |
| 186 | + |
| 187 | + // μλΉμ€λͺ
/λΈλλλͺ
κΈμ§μ΄ μ²΄ν¬ |
| 188 | + if NicknameBannedWords.containsServiceName(nickname) { |
| 189 | + return .serviceNameWord |
| 190 | + } |
| 191 | + |
| 192 | + // μμ€/λΉμμ΄ μ²΄ν¬ |
| 193 | + if NicknameBannedWords.containsProfanity(nickname) { |
| 194 | + return .profanityWord |
| 195 | + } |
| 196 | + |
| 197 | + return nil |
| 198 | + } |
| 199 | +} |
0 commit comments