diff --git a/EATSSU/App/Resources/en.lproj/Localizable.strings b/EATSSU/App/Resources/en.lproj/Localizable.strings new file mode 100644 index 00000000..6b8944e4 --- /dev/null +++ b/EATSSU/App/Resources/en.lproj/Localizable.strings @@ -0,0 +1,461 @@ +// +// Localizable.strings +// EATSSU +// +// Created by jeongminji on 5/3/26. +// + +// MARK: - Common + +/// "숭실대에서 먹자" +"common.logoSubTitle" = "Eat at Soongsil"; +/// "확인" +"common.confirm" = "Confirm"; +/// "취소" +"common.cancel" = "Cancel"; +/// "취소하기" +"common.cancelDark" = "Cancel"; +/// "삭제하기" +"common.delete" = "Delete"; +/// "수정하기" +"common.fix" = "Edit"; +/// "로그인이 필요한 서비스입니다" +"common.needLogin" = "Login is required to use this service."; +/// "로그인 하시겠습니까?" +"common.askLogin" = "Would you like to log in?"; +/// "설정으로 이동" +"common.moveToSetting" = "Go to Settings"; +/// "탈퇴 처리가 완료되었습니다." +"common.withdrawComplete" = "Your account has been deleted."; +/// "잠시 후 다시 시도해주세요." +"common.tryAgain" = "Please try again later."; +/// "세션이 만료되었습니다. 다시 로그인해주세요." +"common.sessionExpired" = "Your session has expired. Please log in again."; +/// "에러가 발생했습니다" +"common.errorOccured" = "An error occurred."; +/// "다시 시도하세요" +"common.retry" = "Try Again"; + + +// MARK: - TabBar + +/// "학식" +"tabBar.meal" = "Cafeteria"; +/// "지도" +"tabBar.map" = "Map"; +/// "나만아니면돼~" +"tabBar.coffee" = "Lucky Game"; +/// "마이" +"tabBar.my" = "My"; + + +// MARK: - Auth + +/// "닉네임을 입력해주세요" +"auth.inputNickName" = "Please enter a nickname"; +/// "Apple로 로그인" +"auth.signInWithApple" = "Sign in with Apple"; +/// "카카오 로그인" +"auth.signInWithKakao" = "Login with Kakao"; +/// "둘러보기" +"auth.lookingWithNoSignIn" = "Browse Without Signing In"; +/// "최근에 로그인했어요" +"auth.lastLoginTooltip" = "Recently used"; +/// LoginVC - "카카오톡으로 생성된 계정입니다." +"auth.kakaoAccount" = "This account was created with KakaoTalk."; +/// LoginVC - "Apple로 생성된 계정입니다." +"auth.appleAccount" = "This account was created with Apple."; +/// SetNickNameView - "닉네임 설정" +"auth.setNickname" = "Edit Nickname"; +/// SetNickNameView - "중복 확인" +"auth.checkDuplicate" = "Check Availability"; +/// SetNickNameView - "소속 설정" +"auth.setCollege" = "Set Affiliation"; +/// SetNickNameView - "단과대" +"auth.college" = "College"; +/// SetNickNameView - "학과" +"auth.department" = "Department"; +/// SetNickNameView - "연결된 계정" +"auth.linkedAccount" = "Linked Account"; +/// SetNickNameView - "없음" +"auth.empty" = "None"; +/// SetNickNameView - "저장하기" +"auth.save" = "Save"; +/// SetNickNameView - "카카오" +"auth.kakao" = "Kakao"; +/// SetNickNameView - "APPLE" +"auth.apple" = "APPLE"; +/// SetNickNameVC - "변경된 정보가 없습니다." +"auth.noChanges" = "No information has changed."; +/// SetNickNameVC - "유효하지 않은 학과 정보입니다." +"auth.invalidDepartment" = "Invalid department information."; +/// SetNickNameVC - "정보 업데이트 중 오류가 발생했습니다." +"auth.updateError" = "An error occurred while updating your information."; +/// SetNickNameVC - "내 정보가 수정되었어요." +"auth.updateSuccess" = "Your information has been updated."; +/// NIcknameTextFieldResultType - "필수 입력 사항입니다" +"auth.requiredInput" = "Required field"; +/// NIcknameTextFieldResultType - "중복 확인을 진행해주세요." +"auth.needCheckDuplicate" = "Please check availability."; +/// NIcknameTextFieldResultType - "이미 사용 중인 닉네임이에요." +"auth.duplicatedNickname" = "This nickname is already in use."; +/// NIcknameTextFieldResultType - "사용가능한 닉네임이에요" +"auth.availableNickname" = "This nickname is available."; +/// NIcknameTextFieldResultType - "2~16글자를 입력해 주세요." +"auth.nicknameLength" = "Enter 2–16 characters"; +/// NIcknameTextFieldResultType - "특수문자로 시작/끝나는 닉네임은 사용할 수 없어요." +"auth.specialCharNickname" = "Nicknames cannot start or end with a special character."; +/// NIcknameTextFieldResultType - "연속된 특수문자(--, __)는 사용할 수 없어요." +"auth.continuousSpecialChar" = "Consecutive special characters (--, __) are not allowed."; +/// NIcknameTextFieldResultType - "숫자만으로 된 닉네임은 사용할 수 없어요." +"auth.numberOnlyNickname" = "Nicknames made only of numbers are not allowed."; +/// NIcknameTextFieldResultType - "허용 문자(한글/영문/숫자)만 사용할 수 있어요." +"auth.allowedChar" = "Only Korean/English letters and numbers are allowed."; +/// NIcknameTextFieldResultType - "사용할 수 없는 단어가 포함되어 있어요." +"auth.bannedWord" = "This nickname contains a word that cannot be used."; +/// NIcknameTextFieldResultType - "띄어쓰기로 시작/끝나는 닉네임은 사용할 수 없어요." +"auth.spaceNickname" = "Nicknames cannot start or end with a space."; +/// NIcknameTextFieldResultType - "연속된 띄어쓰기는 사용할 수 없어요." +"auth.continuousSpace" = "Consecutive spaces are not allowed."; +/// NIcknameTextFieldResultType - "이모지, 특수문자는 사용할 수 없어요." +"auth.emojiSpecialChar" = "Emoji and special characters are not allowed."; +/// NIcknameTextFieldResultType - "관리자로 혼동될 수 있는 닉네임은 사용할 수 없어요." +"auth.adminNickname" = "Nicknames that may be confused with an admin are not allowed."; +/// NIcknameTextFieldResultType - "서비스명 단독 닉네임은 사용할 수 없어요." +"auth.serviceNameNickname" = "You cannot use the service name by itself as a nickname."; +/// NIcknameTextFieldResultType - "욕설, 비속어 등의 표현이 포함된 닉네임은 사용할 수 없어요." +"auth.slangNickname" = "Nicknames containing profanity or slang are not allowed."; + + +// MARK: - Home +/// Home - "오늘의 메뉴" +"home.todayMenu" = "Today's Menu"; +/// Home - "가격" +"home.price" = "Price"; +/// Home - "평점" +"home.rating" = "Rating"; +/// Home - " -" +"home.emptyRating" = " -"; +/// Home - "제공되는 메뉴가 없습니다" +"home.noMenuProvidedMessage" = "No menu is available."; +/// CustomTimeTabController - "아침" +"home.morning" = "Breakfast"; +/// CustomTimeTabController - "점심" +"home.lunch" = "Lunch"; +/// CustomTimeTabController - "저녁" +"home.dinner" = "Dinner"; +/// RestaurantInfoView - "학생 식당" +"home.studentRestaurant" = "Student Restaurant"; +/// RestaurantInfoView - "식당 위치" +"home.restaurantLocation" = "Location"; +/// RestaurantInfoView - "식당 사진" +"home.restaurantPicture" = "Photos"; +/// RestaurantInfoView - "숭실대학교" +"home.soongsilUniversity" = "Soongsil University"; +/// RestaurantInfoView - "영업 시간" +"home.businessHour" = "Hours"; +/// RestaurantInfoView - "비고" +"home.note" = "Notes"; +/// RestaurantInfoView - "아시안푸드, 돈까스, 샐러드, 국밥 등\n카페" +"home.dodamEtc" = "Asian food, pork cutlet, salad, gukbap, etc.\nCafe"; +/// RestaurantMenuGroupCell - "영업 시간이 아니에요." +"home.notBusinessHour" = "Closed now"; +/// RestaurantTableViewHeader - "기숙사 식당" +"home.dormitoryRestaurant" = "Dormitory Restaurant"; + + +// MARK: - Map + +/// MainMapVC - "제휴 지도" +"map.map" = "Partnership Map"; +/// MainMapView - "전체" +"map.all" = "All"; +/// MainMapView - "내 제휴" +"map.myPartner" = "My Partnerships"; +/// NoDepartmentSheetVC - "학과를 입력하고\n나만의 제휴를 확인해보세요!" +"map.inputDepartment" = "Enter your department\nand check your own partnerships!"; +/// NoDepartmentSheetVC - "학과 입력하기" +"map.inputDepartmentButton" = "Enter Department"; +/// PartnershipDetailSheetVC - "음식점" +"map.restaurant" = "Restaurant"; +/// PartnershipDetailSheetVC - "카페" +"map.cafe" = "Cafe"; +/// PartnershipDetailSheetVC - "주점" +"map.pub" = "Pub"; +/// PartnershipDetailSheetVC - "학과 정보 없음" +"map.noDepartmentInfo" = "No Department Info"; +/// MainMapVC+Location - "위치 권한 필요" +"map.needLocationAuth" = "Location Permission Required"; +/// MainMapVC+Location - "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요." +"map.locationAuthDescription" = "Please allow location permission so you can check your location on the map and easily find nearby partners."; + + +// MARK: - MyPage + +/// "마이페이지" +"myPage.myPage" = "My Page"; +/// UserWithdrawVC - "회원탈퇴" +"myPage.withdraw" = "Delete Account"; +/// MyPageVC - "EAT-SSU 수신 동의" +"myPage.agreeNoti" = "EAT-SSU notifications enabled (%@)"; +/// MyPageVC - "EAT-SSU 수신 거절" +"myPage.disagreeNoti" = "EAT-SSU notifications disabled (%@)"; + +// MARK: - MyPageSection: 알림 및 활동 +/// MyPageSectionVC - "알림 및 활동" +"myPage.activitySection" = "Notifications & Activity"; +/// "내 정보" +"myPage.myInfo" = "My Info"; +/// "내 리뷰" +"myPage.myReview" = "My Reviews"; +/// NotificationSettingTableViewCell - "푸시 알림 설정" +"myPage.pushNotificationSetting" = "Push Notification Settings"; +/// NotificationSettingTableViewCell - "매일 오전 11시에 알림을 보내드려요" +"myPage.pushNotificationDescription" = "We'll send you a notification every day at 11 AM."; +/// MyPageVC - "알림 설정 중 오류가 발생했습니다." +"myPage.notiSettingError" = "An error occurred while updating notification settings."; + +// MARK: - MyPageSection: 서비스 정보 +// MyPageSectionVC - "서비스 정보" +"myPage.serviceInfoSection" = "Service Info"; +/// MyPageVC - "문의하기" +"myPage.inquiry" = "Contact Us"; +/// CreatorVC - "만든 사람들" +"myPage.creators" = "Creators"; +/// MyPageVC - "EAT-SSU 인스타그램" +"myPage.instagram" = "EAT-SSU Instagram"; + +// MARK: - MyPageSection: 기타 +// MyPageSectionVC - "기타" +"myPage.etcSection" = "Other"; +/// MyPageVC - "언어 설정" +"myPage.languageSetting" = "Language"; +/// MyPageVC - "지원 언어: 한국어" +"myPage.language.korean" = "Korean"; +/// MyPageVC - "지원 언어: 영어" +"myPage.language.english" = "English"; +/// MyPageVC - "약관 및 정책" +"myPage.termsAndPolicy" = "Terms & Policies"; +/// MyPageVC - "서비스 이용약관" +"myPage.termsOfUse" = "Terms of Service"; +/// MyPageVC - "개인정보처리방침" +"myPage.privacyTermsOfUse" = "Privacy Policy"; +/// MyPageVC - "로그아웃" +"myPage.logout" = "Log Out"; +/// MyPageVC - "정말 로그아웃 하시겠습니까?" +"myPage.askLogout" = "Are you sure you want to log out?"; + +/// MyReviewVC - "리뷰 수정 혹은 삭제" +"myPage.fixOrDeleteReview" = "Edit or Delete Review"; +/// MyReviewVC - "작성하신 리뷰를 수정 또는 삭제하시겠습니까?" +"myPage.askFixOrDeleteReview" = "Would you like to edit or delete this review?"; +/// MyReviewVC - "리뷰 삭제하기" +"myPage.deleteMyReview" = "Delete Review"; +/// MyReviewVC - "해당 리뷰를 삭제할까요?" +"myPage.askDeleteMyReview" = "Delete this review?"; +/// MyReviewVC - "리뷰가 성공적으로 삭제되었습니다." +"myPage.deleteMyReviewSuccess" = "Your review has been deleted successfully."; +/// MyPageView - "다시 시도해주세요" +"myPage.retry" = "Please try again."; +/// MyPageView - "앱 버전" +"myPage.appVersion" = "App Version"; +/// MyPageView - "탈퇴하기" +"myPage.withdrawButton" = "Delete Account"; +/// MyPageView - "알 수 없음" +"myPage.unknownUser" = "Unknown"; +/// ProvisionVC - "이용약관" +"myPage.defaultTerms" = "Terms of Service"; +/// UserWithdrawView - "정말 탈퇴하시겠습니까?" +"myPage.confirmWithdrawal" = "Are you sure you want to delete your account?"; +/// UserWithdrawView - "작성한 리뷰 게시글은 삭제되지 않으며, (알수없음)으로 표시됩니다.\n자세한 내용은 서비스이용약관 및 개인정보처리방침을 확인해 주세요." +"myPage.withdrawalNotice" = "Your posted reviews will not be deleted and will be shown as (Unknown).\nFor details, please check the Terms of Service and Privacy Policy."; +/// UserWithdrawView - "올바른 입력입니다." +"myPage.validInputMessage" = "Valid input."; +/// UserWithdrawView - "올바르지 않은 닉네임입니다" +"myPage.invalidNicknameMessage" = "Invalid nickname."; + + +// MARK: - Review + +/// ReportVC - "EAT SSU 팀에게 보내기" +"review.sendToTeam" = "Send to the EAT SSU Team"; +/// ReportVC - "신고하기" +"review.report" = "Report"; +/// ReportVC - "사유를 선택해주세요!" +"review.selectReason" = "Please select a reason!"; +/// ReportVC - "신고가 성공적으로 접수되었어요!" +"review.reportSuccess" = "Your report has been submitted successfully!"; +/// ReportVC, ReportView - "메뉴와 관련없는 내용" +"review.unrelatedMenu" = "Content unrelated to the menu"; +/// ReportVC, ReportView - "음란성, 욕설 등 부적절한 내용" +"review.inappropriateContent" = "Inappropriate content such as profanity or explicit content"; +/// ReportVC, ReportView - "부적절한 홍보 또는 광고" +"review.inappropriateAd" = "Inappropriate promotion or advertisement"; +/// ReportVC, ReportView - "리뷰 작성 취지에 맞지 않는 내용 (복사글 등)" +"review.notReviewFormat" = "Content that does not fit the purpose of a review (e.g. copied text)"; +/// ReportVC, ReportView - "저작권 도용 의심 (사진 등)" +"review.copyright" = "Suspected copyright infringement (e.g. photos)"; +/// ReportVC, ReportView - "기타 (하단 내용 작성)" +"review.etc" = "Other (write details below)"; +/// ReportView - "리뷰 신고 사유를 알려주세요" +"review.reportReason" = "Tell us why you are reporting this review"; +/// ReportView - "하나의 리뷰에 대해 24시간 내 한 번만 신고 가능합니다." +"review.reportGuide" = "You can report the same review only once every 24 hours."; +/// ReportView - "리뷰 신고 사유를 작성해 주세요" +"review.inputReportReason" = "Please enter the reason for reporting this review."; +/// ReviewVC - "리뷰 작성하기" +"review.writeReview" = "Write a Review"; +/// ReviewVC - "리뷰가 성공적으로 등록되었습니다." +"review.registerReviewSuccess" = "Your review has been posted successfully."; +/// ReviewVC - "리뷰" +"review.review" = "Reviews"; +/// ReviewVC - "리뷰 삭제" +"review.deleteReview" = "Delete Review"; +/// ReviewVC - "해당 리뷰를 삭제할까요?" +"review.askDeleteReview" = "Delete this review?"; +/// ReviewVC - "리뷰 신고하기" +"review.reportReview" = "Report Review"; +/// ReviewVC - "해당 리뷰를 신고하시겠습니까?" +"review.askReportReview" = "Would you like to report this review?"; +/// ReviewVC - "리뷰가 성공적으로 삭제되었습니다." +"review.deleteReviewSuccess" = "Your review has been deleted successfully."; +/// ReviewVC - "리뷰 삭제에 실패했습니다." +"review.deleteReviewFail" = "Failed to delete the review."; +/// SetRateVC - "리뷰 수정하기" +"review.fixReview" = "Edit Review"; +/// SetRateVC - "리뷰 남기기" +"review.leaveReview" = "Submit Review"; +/// 메뉴 이름의 받침 유무에 따라 '을/를'을 동적으로 붙여 추천 문장을 생성합니다. +"review.recommendMenu.default" = "Would you recommend %@?"; +/// 메뉴 이름의 받침 유무에 따라 '을/를'을 동적으로 붙여 추천 문장을 생성합니다. +"review.recommendMenu.withJongseong" = "Would you recommend %@?"; +/// 메뉴 이름의 받침 유무에 따라 '을/를'을 동적으로 붙여 추천 문장을 생성합니다. +"review.recommendMenu.withoutJongseong" = "Would you recommend %@?"; +/// SetRateVC - "메뉴를 추천하시겠어요?" +"review.recommendMenuTitle" = "Would you recommend this menu?"; +/// SetRateVC - "리뷰 수정 완료하기" +"review.fixReviewComplete" = "Complete Review Edit"; +/// SetRateVC - "완료하기" +"review.complete" = "Complete"; +/// SetRateVC - "별점을 입력해주세요!" +"review.inputRating" = "Please enter a rating!"; +/// SetRateVC - "메뉴 목록 조회에 실패했습니다." +"review.loadMenuListFail" = "Failed to load the menu list."; +/// SetRateVC - "수정할 리뷰 정보가 없습니다." +"review.noReviewInfoForFix" = "There is no review information to edit."; +/// SetRateVC - "리뷰가 성공적으로 수정되었습니다." +"review.fixReviewSuccess" = "Your review has been updated successfully."; +/// SetRateVC - "리뷰 수정에 실패했습니다." +"review.fixReviewFail" = "Failed to update the review."; +/// SetRateVC - "식단 정보가 없습니다." +"review.noMealInfo" = "No meal information is available."; +/// SetRateVC - "리뷰 업로드에 실패했습니다." +"review.uploadReviewFail" = "Failed to upload the review."; +/// SetRateVC - "메뉴 정보가 없습니다." +"review.noMenuInfo" = "No menu information is available."; +/// SetRateVC - "메뉴에 대한 상세한 리뷰를 작성해주세요" +"review.inputDetailReview" = "Please write a detailed review of the menu."; +/// SetRateVC - "나가시겠어요?" +"review.askLeave" = "Leave this page?"; +/// SetRateVC - "지금 나가면 작성한 내용이 저장되지 않습니다." +"review.leaveWarning" = "Your changes will not be saved if you leave now."; +/// SetRateVC - "나가기" +"review.leave" = "Leave"; +/// SetRateVC - "계속 작성" +"review.continueWriting" = "Keep Writing"; +/// ReviewEmptyViewCell - "아직 작성된 리뷰가 없어요!" +"review.noReview" = "No reviews have been written yet!"; +/// ReviewEmptyViewCell - "메뉴에 가장 먼저 리뷰를 남겨주세요!" +"review.beFirstReviewer" = "Be the first to leave a review for this menu!"; +/// ReviewEmptyViewCell - "로그인이 필요합니다" +"review.needLogin" = "Login is required."; +/// ReviewEmptyViewCell - "로그인 후 리뷰를 확인하세요" +"review.checkReviewAfterLogin" = "Log in to view reviews."; +/// ReviewEmptyViewCell - "아직 작성한 리뷰가 없어요" +"review.noWrittenReview" = "You haven't written any reviews yet."; +/// ReviewEmptyViewCell - "첫 리뷰를 남겨 주세요!" +"review.writeFirstReview" = "Leave your first review!"; +/// ReviewDividerCell - "리뷰" +"review.reviewCount" = "%d Reviews"; +/// ReviewRateViewCell - "오늘의 메뉴" +"review.todayMenu" = "Today's Menu"; +/// ReviewRateViewCell - "5점" +"review.fiveStars" = "5 Stars"; +/// ReviewRateViewCell - "4점" +"review.fourStars" = "4 Stars"; +/// ReviewRateViewCell - "3점" +"review.threeStars" = "3 Stars"; +/// ReviewRateViewCell - "2점" +"review.twoStars" = "2 Stars"; +/// ReviewRateViewCell - "1점" +"review.oneStar" = "1 Star"; +/// SetRateView - "오늘의 식사는 어떠셨나요?" +"review.rateTodayMeal" = "How was your meal today?"; +/// SetRateView - "추천하고 싶은 메뉴가 있나요?" +"review.recommendMenu" = "Any dishes you'd recommend?"; +/// SetRateView - "사진 추가 (0/1)" +"review.addPhoto" = "Add Photo (%d/1)"; +/// character count +"review.characterCount" = "%d / %d"; + + +// MARK: - Coffee + +/// "나가시겠어요?" +"coffee.askLeave" = "Leave this page?"; +/// "지금 나가면 진행 상황이\n저장되지 않습니다." +"coffee.leaveWarning" = "Your progress will not be saved\nif you leave now."; +/// "나가기" +"coffee.leave" = "Leave"; +/// "계속하기" +"coffee.continueEvent" = "Continue"; + + +// MARK: - Splash + +/// NoticeSplashVC - "긴급 서버 점검 안내" +"splash.serverInspection" = "Emergency Server Maintenance Notice"; + + +// MARK: - PromotionPopup + +/// 03. 16(월)~03. 27(금) +"promotionPopup.period" = "03. 16(Mon)~03. 27(Fri)"; +/// EAT-SSU 인스타그램 바로가기 +"promotionPopup.instagramButtonTitle" = "Go to EAT-SSU Instagram"; +/// 자세한 내용은 EAT-SSU 인스타그램을 확인해 주세요 +"promotionPopup.guideMessage" = "Please check EAT-SSU Instagram for more details."; +/// 다시 보지 않기 +"promotionPopup.neverShowAgain" = "Don't show again"; +/// 닫기 +"promotionPopup.close" = "Close"; + + +// MARK: - Notification + +/// 🤔 오늘 밥 뭐 먹지… +"notification.dailyWeekdayNotificationTitle" = "🤔 What should I eat today…"; +/// 오늘의 학식을 확인해보세요! +"notification.dailyWeekdayNotificationBody" = "Check today's cafeteria menu!"; +/// 알림 권한 필요 +"notification_error_permission_denied_message" = "Notification Permission Required"; +/// 알림을 받으려면 설정에서 알림 권한을 허용해주세요. +"notification_error_permission_denied_description" = "Please allow notification permission in Settings to receive notifications."; +/// 알 수 없는 오류 +"notification_error_unknown_message" = "Unknown Error"; +/// 다시 시도해주세요. +"notification_error_unknown_description" = "Please try again."; + + +// MARK: - Restaurant + +/// "기숙사 식당" +"restaurant.dormitoryRestaurant" = "Dormitory Restaurant"; +/// "도담 식당" +"restaurant.dodamRestaurant" = "Dodam Restaurant"; +/// "학생 식당" +"restaurant.studentRestaurant" = "Student Restaurant"; +/// "스낵 코너" +"restaurant.snackCorner" = "Snack Corner"; +/// "FACULTY (교직원 전용)" +"restaurant.facultyRestaurant" = "FACULTY (Faculty Only)"; diff --git a/EATSSU/App/Resources/ko.lproj/Localizable.strings b/EATSSU/App/Resources/ko.lproj/Localizable.strings new file mode 100644 index 00000000..775190d3 --- /dev/null +++ b/EATSSU/App/Resources/ko.lproj/Localizable.strings @@ -0,0 +1,462 @@ +// +// Localizable.strings +// EATSSU +// +// Created by jeongminji on 5/3/26. +// + +// MARK: - Common + +/// "숭실대에서 먹자" +"common.logoSubTitle" = "숭실대에서 먹자"; +/// "확인" +"common.confirm" = "확인"; +/// "취소" +"common.cancel" = "취소"; +/// "취소하기" +"common.cancelDark" = "취소하기"; +/// "삭제하기" +"common.delete" = "삭제하기"; +/// "수정하기" +"common.fix" = "수정하기"; +/// "로그인이 필요한 서비스입니다" +"common.needLogin" = "로그인이 필요한 서비스입니다"; +/// "로그인 하시겠습니까?" +"common.askLogin" = "로그인 하시겠습니까?"; +/// "설정으로 이동" +"common.moveToSetting" = "설정으로 이동"; +/// "탈퇴 처리가 완료되었습니다." +"common.withdrawComplete" = "탈퇴 처리가 완료되었습니다."; +/// "잠시 후 다시 시도해주세요." +"common.tryAgain" = "잠시 후 다시 시도해주세요."; +/// "세션이 만료되었습니다. 다시 로그인해주세요." +"common.sessionExpired" = "세션이 만료되었습니다. 다시 로그인해주세요."; +/// "에러가 발생했습니다" +"common.errorOccured" = "에러가 발생했습니다"; +/// "다시 시도하세요" +"common.retry" = "다시 시도하세요"; + + +// MARK: - TabBar + +/// "학식" +"tabBar.meal" = "학식"; +/// "지도" +"tabBar.map" = "지도"; +/// "나만아니면돼~" +"tabBar.coffee" = "나만아니면돼~"; +/// "마이" +"tabBar.my" = "마이"; + + +// MARK: - Auth + +/// "닉네임을 입력해주세요" +"auth.inputNickName" = "닉네임을 입력해주세요"; +/// "Apple로 로그인" +"auth.signInWithApple" = "Apple로 로그인"; +/// "카카오 로그인" +"auth.signInWithKakao" = "카카오 로그인"; +/// "둘러보기" +"auth.lookingWithNoSignIn" = "둘러보기"; +/// "최근에 로그인했어요" +"auth.lastLoginTooltip" = "최근에 로그인했어요"; +/// LoginVC - "카카오톡으로 생성된 계정입니다." +"auth.kakaoAccount" = "카카오톡으로 생성된 계정입니다."; +/// LoginVC - "Apple로 생성된 계정입니다." +"auth.appleAccount" = "Apple로 생성된 계정입니다."; +/// SetNickNameView - "닉네임 설정" +"auth.setNickname" = "닉네임 설정"; +/// SetNickNameView - "중복 확인" +"auth.checkDuplicate" = "중복 확인"; +/// SetNickNameView - "소속 설정" +"auth.setCollege" = "소속 설정"; +/// SetNickNameView - "단과대" +"auth.college" = "단과대"; +/// SetNickNameView - "학과" +"auth.department" = "학과"; +/// SetNickNameView - "연결된 계정" +"auth.linkedAccount" = "연결된 계정"; +/// SetNickNameView - "없음" +"auth.empty" = "없음"; +/// SetNickNameView - "저장하기" +"auth.save" = "저장하기"; +/// SetNickNameView - "카카오" +"auth.kakao" = "카카오"; +/// SetNickNameView - "APPLE" +"auth.apple" = "APPLE"; +/// SetNickNameVC - "변경된 정보가 없습니다." +"auth.noChanges" = "변경된 정보가 없습니다."; +/// SetNickNameVC - "유효하지 않은 학과 정보입니다." +"auth.invalidDepartment" = "유효하지 않은 학과 정보입니다."; +/// SetNickNameVC - "정보 업데이트 중 오류가 발생했습니다." +"auth.updateError" = "정보 업데이트 중 오류가 발생했습니다."; +/// SetNickNameVC - "내 정보가 수정되었어요." +"auth.updateSuccess" = "내 정보가 수정되었어요."; +/// NIcknameTextFieldResultType - "필수 입력 사항입니다" +"auth.requiredInput" = "필수 입력 사항입니다"; +/// NIcknameTextFieldResultType - "중복 확인을 진행해주세요." +"auth.needCheckDuplicate" = "중복 확인을 진행해주세요."; +/// NIcknameTextFieldResultType - "이미 사용 중인 닉네임이에요." +"auth.duplicatedNickname" = "이미 사용 중인 닉네임이에요."; +/// NIcknameTextFieldResultType - "사용가능한 닉네임이에요" +"auth.availableNickname" = "사용가능한 닉네임이에요"; +/// NIcknameTextFieldResultType - "2~16글자를 입력해 주세요." +"auth.nicknameLength" = "2~16글자를 입력해 주세요."; +/// NIcknameTextFieldResultType - "특수문자로 시작/끝나는 닉네임은 사용할 수 없어요." +"auth.specialCharNickname" = "특수문자로 시작/끝나는 닉네임은 사용할 수 없어요."; +/// NIcknameTextFieldResultType - "연속된 특수문자(--, __)는 사용할 수 없어요." +"auth.continuousSpecialChar" = "연속된 특수문자(--, __)는 사용할 수 없어요."; +/// NIcknameTextFieldResultType - "숫자만으로 된 닉네임은 사용할 수 없어요." +"auth.numberOnlyNickname" = "숫자만으로 된 닉네임은 사용할 수 없어요."; +/// NIcknameTextFieldResultType - "허용 문자(한글/영문/숫자)만 사용할 수 있어요." +"auth.allowedChar" = "허용 문자(한글/영문/숫자)만 사용할 수 있어요."; +/// NIcknameTextFieldResultType - "사용할 수 없는 단어가 포함되어 있어요." +"auth.bannedWord" = "사용할 수 없는 단어가 포함되어 있어요."; +/// NIcknameTextFieldResultType - "띄어쓰기로 시작/끝나는 닉네임은 사용할 수 없어요." +"auth.spaceNickname" = "띄어쓰기로 시작/끝나는 닉네임은 사용할 수 없어요."; +/// NIcknameTextFieldResultType - "연속된 띄어쓰기는 사용할 수 없어요." +"auth.continuousSpace" = "연속된 띄어쓰기는 사용할 수 없어요."; +/// NIcknameTextFieldResultType - "이모지, 특수문자는 사용할 수 없어요." +"auth.emojiSpecialChar" = "이모지, 특수문자는 사용할 수 없어요."; +/// NIcknameTextFieldResultType - "관리자로 혼동될 수 있는 닉네임은 사용할 수 없어요." +"auth.adminNickname" = "관리자로 혼동될 수 있는 닉네임은 사용할 수 없어요."; +/// NIcknameTextFieldResultType - "서비스명 단독 닉네임은 사용할 수 없어요." +"auth.serviceNameNickname" = "서비스명 단독 닉네임은 사용할 수 없어요."; +/// NIcknameTextFieldResultType - "욕설, 비속어 등의 표현이 포함된 닉네임은 사용할 수 없어요." +"auth.slangNickname" = "욕설, 비속어 등의 표현이 포함된 닉네임은 사용할 수 없어요."; + + +// MARK: - Home + +/// Home - "오늘의 메뉴" +"home.todayMenu" = "오늘의 메뉴"; +/// Home - "가격" +"home.price" = "가격"; +/// Home - "평점" +"home.rating" = "평점"; +/// Home - " -" +"home.emptyRating" = " -"; +/// Home - "제공되는 메뉴가 없습니다" +"home.noMenuProvidedMessage" = "제공되는 메뉴가 없습니다"; +/// CustomTimeTabController - "아침" +"home.morning" = "아침"; +/// CustomTimeTabController - "점심" +"home.lunch" = "점심"; +/// CustomTimeTabController - "저녁" +"home.dinner" = "저녁"; +/// RestaurantInfoView - "학생 식당" +"home.studentRestaurant" = "학생 식당"; +/// RestaurantInfoView - "식당 위치" +"home.restaurantLocation" = "식당 위치"; +/// RestaurantInfoView - "식당 사진" +"home.restaurantPicture" = "식당 사진"; +/// RestaurantInfoView - "숭실대학교" +"home.soongsilUniversity" = "숭실대학교"; +/// RestaurantInfoView - "영업 시간" +"home.businessHour" = "영업 시간"; +/// RestaurantInfoView - "비고" +"home.note" = "비고"; +/// RestaurantInfoView - "아시안푸드, 돈까스, 샐러드, 국밥 등\n카페" +"home.dodamEtc" = "아시안푸드, 돈까스, 샐러드, 국밥 등\n카페"; +/// RestaurantMenuGroupCell - "영업 시간이 아니에요." +"home.notBusinessHour" = "영업 시간이 아니에요."; +/// RestaurantTableViewHeader - "기숙사 식당" +"home.dormitoryRestaurant" = "기숙사 식당"; + + +// MARK: - Map + +/// MainMapVC - "제휴 지도" +"map.map" = "제휴 지도"; +/// MainMapView - "전체" +"map.all" = "전체"; +/// MainMapView - "내 제휴" +"map.myPartner" = "내 제휴"; +/// NoDepartmentSheetVC - "학과를 입력하고\n나만의 제휴를 확인해보세요!" +"map.inputDepartment" = "학과를 입력하고\n나만의 제휴를 확인해보세요!"; +/// NoDepartmentSheetVC - "학과 입력하기" +"map.inputDepartmentButton" = "학과 입력하기"; +/// PartnershipDetailSheetVC - "음식점" +"map.restaurant" = "음식점"; +/// PartnershipDetailSheetVC - "카페" +"map.cafe" = "카페"; +/// PartnershipDetailSheetVC - "주점" +"map.pub" = "주점"; +/// PartnershipDetailSheetVC - "학과 정보 없음" +"map.noDepartmentInfo" = "학과 정보 없음"; +/// MainMapVC+Location - "위치 권한 필요" +"map.needLocationAuth" = "위치 권한 필요"; +/// MainMapVC+Location - "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요." +"map.locationAuthDescription" = "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요."; + + +// MARK: - MyPage + +/// "마이페이지" +"myPage.myPage" = "마이페이지"; +/// UserWithdrawVC - "회원탈퇴" +"myPage.withdraw" = "회원탈퇴"; +/// MyPageVC - "EAT-SSU 수신 동의" +"myPage.agreeNoti" = "EAT-SSU 수신 동의 (%@)"; +/// MyPageVC - "EAT-SSU 수신 거절" +"myPage.disagreeNoti" = "EAT-SSU 수신 거절 (%@)"; + +// MARK: - MyPageSection: 알림 및 활동 +/// MyPageSectionVC - "알림 및 활동" +"myPage.activitySection" = "알림 및 활동"; +/// "내 정보" +"myPage.myInfo" = "내 정보"; +/// "내 리뷰" +"myPage.myReview" = "내 리뷰"; +/// NotificationSettingTableViewCell - "푸시 알림 설정" +"myPage.pushNotificationSetting" = "푸시 알림 설정"; +/// NotificationSettingTableViewCell - "매일 오전 11시에 알림을 보내드려요" +"myPage.pushNotificationDescription" = "매일 오전 11시에 알림을 보내드려요"; +/// MyPageVC - "알림 설정 중 오류가 발생했습니다." +"myPage.notiSettingError" = "알림 설정 중 오류가 발생했습니다."; + +// MARK: - MyPageSection: 서비스 정보 +// MyPageSectionVC - "서비스 정보" +"myPage.serviceInfoSection" = "서비스 정보"; +/// MyPageVC - "문의하기" +"myPage.inquiry" = "문의하기"; +/// CreatorVC - "만든 사람들" +"myPage.creators" = "만든 사람들"; +/// MyPageVC - "EAT-SSU 인스타그램" +"myPage.instagram" = "EAT-SSU 인스타그램"; + +// MARK: - MyPageSection: 기타 +// MyPageSectionVC - "기타" +"myPage.etcSection" = "기타"; +/// MyPageVC - "언어 설정" +"myPage.languageSetting" = "언어 설정"; +/// MyPageVC - "지원 언어: 한국어" +"myPage.language.korean" = "한국어"; +/// MyPageVC - "지원 언어: 영어" +"myPage.language.english" = "English"; +/// MyPageVC - "약관 및 정책" +"myPage.termsAndPolicy" = "약관 및 정책"; +/// MyPageVC - "서비스 이용약관" +"myPage.termsOfUse" = "서비스 이용약관"; +/// MyPageVC - "개인정보처리방침" +"myPage.privacyTermsOfUse" = "개인정보처리방침"; +/// MyPageVC - "로그아웃" +"myPage.logout" = "로그아웃"; +/// MyPageVC - "정말 로그아웃 하시겠습니까?" +"myPage.askLogout" = "정말 로그아웃 하시겠습니까?"; + +/// MyReviewVC - "리뷰 수정 혹은 삭제" +"myPage.fixOrDeleteReview" = "리뷰 수정 혹은 삭제"; +/// MyReviewVC - "작성하신 리뷰를 수정 또는 삭제하시겠습니까?" +"myPage.askFixOrDeleteReview" = "작성하신 리뷰를 수정 또는 삭제하시겠습니까?"; +/// MyReviewVC - "리뷰 삭제하기" +"myPage.deleteMyReview" = "리뷰 삭제하기"; +/// MyReviewVC - "해당 리뷰를 삭제할까요?" +"myPage.askDeleteMyReview" = "해당 리뷰를 삭제할까요?"; +/// MyReviewVC - "리뷰가 성공적으로 삭제되었습니다." +"myPage.deleteMyReviewSuccess" = "리뷰가 성공적으로 삭제되었습니다."; +/// MyPageView - "다시 시도해주세요" +"myPage.retry" = "다시 시도해주세요"; +/// MyPageView - "앱 버전" +"myPage.appVersion" = "앱 버전"; +/// MyPageView - "탈퇴하기" +"myPage.withdrawButton" = "탈퇴하기"; +/// MyPageView - "알 수 없음" +"myPage.unknownUser" = "알 수 없음"; +/// ProvisionVC - "이용약관" +"myPage.defaultTerms" = "이용약관"; +/// UserWithdrawView - "정말 탈퇴하시겠습니까?" +"myPage.confirmWithdrawal" = "정말 탈퇴하시겠습니까?"; +/// UserWithdrawView - "작성한 리뷰 게시글은 삭제되지 않으며, (알수없음)으로 표시됩니다.\n자세한 내용은 서비스이용약관 및 개인정보처리방침을 확인해 주세요." +"myPage.withdrawalNotice" = "작성한 리뷰 게시글은 삭제되지 않으며, (알수없음)으로 표시됩니다.\n자세한 내용은 서비스이용약관 및 개인정보처리방침을 확인해 주세요."; +/// UserWithdrawView - "올바른 입력입니다." +"myPage.validInputMessage" = "올바른 입력입니다"; +/// UserWithdrawView - "올바르지 않은 닉네임입니다" +"myPage.invalidNicknameMessage" = "올바르지 않은 닉네임입니다"; + + +// MARK: - Review + +/// ReportVC - "EAT SSU 팀에게 보내기" +"review.sendToTeam" = "EAT SSU 팀에게 보내기"; +/// ReportVC - "신고하기" +"review.report" = "신고하기"; +/// ReportVC - "사유를 선택해주세요!" +"review.selectReason" = "사유를 선택해주세요!"; +/// ReportVC - "신고가 성공적으로 접수되었어요!" +"review.reportSuccess" = "신고가 성공적으로 접수되었어요!"; +/// ReportVC, ReportView - "메뉴와 관련없는 내용" +"review.unrelatedMenu" = "메뉴와 관련없는 내용"; +/// ReportVC, ReportView - "음란성, 욕설 등 부적절한 내용" +"review.inappropriateContent" = "음란성, 욕설 등 부적절한 내용"; +/// ReportVC, ReportView - "부적절한 홍보 또는 광고" +"review.inappropriateAd" = "부적절한 홍보 또는 광고"; +/// ReportVC, ReportView - "리뷰 작성 취지에 맞지 않는 내용 (복사글 등)" +"review.notReviewFormat" = "리뷰 작성 취지에 맞지 않는 내용 (복사글 등)"; +/// ReportVC, ReportView - "저작권 도용 의심 (사진 등)" +"review.copyright" = "저작권 도용 의심 (사진 등)"; +/// ReportVC, ReportView - "기타 (하단 내용 작성)" +"review.etc" = "기타 (하단 내용 작성)"; +/// ReportView - "리뷰 신고 사유를 알려주세요" +"review.reportReason" = "리뷰 신고 사유를 알려주세요"; +/// ReportView - "하나의 리뷰에 대해 24시간 내 한 번만 신고 가능합니다." +"review.reportGuide" = "하나의 리뷰에 대해 24시간 내 한 번만 신고 가능합니다."; +/// ReportView - "리뷰 신고 사유를 작성해 주세요" +"review.inputReportReason" = "리뷰 신고 사유를 작성해 주세요"; +/// ReviewVC - "리뷰 작성하기" +"review.writeReview" = "리뷰 작성하기"; +/// ReviewVC - "리뷰가 성공적으로 등록되었습니다." +"review.registerReviewSuccess" = "리뷰가 성공적으로 등록되었습니다."; +/// ReviewVC - "리뷰" +"review.review" = "리뷰"; +/// ReviewVC - "리뷰 삭제" +"review.deleteReview" = "리뷰 삭제"; +/// ReviewVC - "해당 리뷰를 삭제할까요?" +"review.askDeleteReview" = "해당 리뷰를 삭제할까요?"; +/// ReviewVC - "리뷰 신고하기" +"review.reportReview" = "리뷰 신고하기"; +/// ReviewVC - "해당 리뷰를 신고하시겠습니까?" +"review.askReportReview" = "해당 리뷰를 신고하시겠습니까?"; +/// ReviewVC - "리뷰가 성공적으로 삭제되었습니다." +"review.deleteReviewSuccess" = "리뷰가 성공적으로 삭제되었습니다."; +/// ReviewVC - "리뷰 삭제에 실패했습니다." +"review.deleteReviewFail" = "리뷰 삭제에 실패했습니다."; +/// SetRateVC - "리뷰 수정하기" +"review.fixReview" = "리뷰 수정하기"; +/// SetRateVC - "리뷰 남기기" +"review.leaveReview" = "리뷰 남기기"; +/// 메뉴 이름의 받침 유무에 따라 '을/를'을 동적으로 붙여 추천 문장을 생성합니다. +"review.recommendMenu.default" = "%@을(를) 추천하시겠어요?"; +/// 메뉴 이름의 받침 유무에 따라 '을/를'을 동적으로 붙여 추천 문장을 생성합니다. +"review.recommendMenu.withJongseong" = "%@을 추천하시겠어요?"; +/// 메뉴 이름의 받침 유무에 따라 '을/를'을 동적으로 붙여 추천 문장을 생성합니다. +"review.recommendMenu.withoutJongseong" = "%@를 추천하시겠어요?"; +/// SetRateVC - "메뉴를 추천하시겠어요?" +"review.recommendMenuTitle" = "메뉴를 추천하시겠어요?"; +/// SetRateVC - "리뷰 수정 완료하기" +"review.fixReviewComplete" = "리뷰 수정 완료하기"; +/// SetRateVC - "완료하기" +"review.complete" = "완료하기"; +/// SetRateVC - "별점을 입력해주세요!" +"review.inputRating" = "별점을 입력해주세요!"; +/// SetRateVC - "메뉴 목록 조회에 실패했습니다." +"review.loadMenuListFail" = "메뉴 목록 조회에 실패했습니다."; +/// SetRateVC - "수정할 리뷰 정보가 없습니다." +"review.noReviewInfoForFix" = "수정할 리뷰 정보가 없습니다."; +/// SetRateVC - "리뷰가 성공적으로 수정되었습니다." +"review.fixReviewSuccess" = "리뷰가 성공적으로 수정되었습니다."; +/// SetRateVC - "리뷰 수정에 실패했습니다." +"review.fixReviewFail" = "리뷰 수정에 실패했습니다."; +/// SetRateVC - "식단 정보가 없습니다." +"review.noMealInfo" = "식단 정보가 없습니다."; +/// SetRateVC - "리뷰 업로드에 실패했습니다." +"review.uploadReviewFail" = "리뷰 업로드에 실패했습니다."; +/// SetRateVC - "메뉴 정보가 없습니다." +"review.noMenuInfo" = "메뉴 정보가 없습니다."; +/// SetRateVC - "메뉴에 대한 상세한 리뷰를 작성해주세요" +"review.inputDetailReview" = "메뉴에 대한 상세한 리뷰를 작성해주세요"; +/// SetRateVC - "나가시겠어요?" +"review.askLeave" = "나가시겠어요?"; +/// SetRateVC - "지금 나가면 작성한 내용이 저장되지 않습니다." +"review.leaveWarning" = "지금 나가면 작성한 내용이 저장되지 않습니다."; +/// SetRateVC - "나가기" +"review.leave" = "나가기"; +/// SetRateVC - "계속 작성" +"review.continueWriting" = "계속 작성"; +/// ReviewEmptyViewCell - "아직 작성된 리뷰가 없어요!" +"review.noReview" = "아직 작성된 리뷰가 없어요!"; +/// ReviewEmptyViewCell - "메뉴에 가장 먼저 리뷰를 남겨주세요!" +"review.beFirstReviewer" = "메뉴에 가장 먼저 리뷰를 남겨주세요!"; +/// ReviewEmptyViewCell - "로그인이 필요합니다" +"review.needLogin" = "로그인이 필요합니다"; +/// ReviewEmptyViewCell - "로그인 후 리뷰를 확인하세요" +"review.checkReviewAfterLogin" = "로그인 후 리뷰를 확인하세요"; +/// ReviewEmptyViewCell - "아직 작성한 리뷰가 없어요" +"review.noWrittenReview" = "아직 작성한 리뷰가 없어요"; +/// ReviewEmptyViewCell - "첫 리뷰를 남겨 주세요!" +"review.writeFirstReview" = "첫 리뷰를 남겨 주세요!"; +/// ReviewDividerCell - "리뷰" +"review.reviewCount" = "리뷰 %d"; +/// ReviewRateViewCell - "오늘의 메뉴" +"review.todayMenu" = "오늘의 메뉴"; +/// ReviewRateViewCell - "5점" +"review.fiveStars" = "5점"; +/// ReviewRateViewCell - "4점" +"review.fourStars" = "4점"; +/// ReviewRateViewCell - "3점" +"review.threeStars" = "3점"; +/// ReviewRateViewCell - "2점" +"review.twoStars" = "2점"; +/// ReviewRateViewCell - "1점" +"review.oneStar" = "1점"; +/// SetRateView - "오늘의 식사는 어떠셨나요?" +"review.rateTodayMeal" = "오늘의 식사는 어떠셨나요?"; +/// SetRateView - "추천하고 싶은 메뉴가 있나요?" +"review.recommendMenu" = "추천하고 싶은 메뉴가 있나요?"; +/// SetRateView - "사진 추가 (0/1)" +"review.addPhoto" = "사진 추가 (%d/1)"; +/// character count +"review.characterCount" = "%d / %d"; + + +// MARK: - Coffee + +/// "나가시겠어요?" +"coffee.askLeave" = "나가시겠어요?"; +/// "지금 나가면 진행 상황이\n저장되지 않습니다." +"coffee.leaveWarning" = "지금 나가면 진행 상황이\n저장되지 않습니다."; +/// "나가기" +"coffee.leave" = "나가기"; +/// "계속하기" +"coffee.continueEvent" = "계속하기"; + + +// MARK: - Splash + +/// NoticeSplashVC - "긴급 서버 점검 안내" +"splash.serverInspection" = "긴급 서버 점검 안내"; + + +// MARK: - PromotionPopup + +/// 03. 16(월)~03. 27(금) +"promotionPopup.period" = "03. 16(월)~03. 27(금)"; +/// EAT-SSU 인스타그램 바로가기 +"promotionPopup.instagramButtonTitle" = "EAT-SSU 인스타그램 바로가기"; +/// 자세한 내용은 EAT-SSU 인스타그램을 확인해 주세요 +"promotionPopup.guideMessage" = "자세한 내용은 EAT-SSU 인스타그램을 확인해 주세요"; +/// 다시 보지 않기 +"promotionPopup.neverShowAgain" = "다시 보지 않기"; +/// 닫기 +"promotionPopup.close" = "닫기"; + + +// MARK: - Notification + +/// 🤔 오늘 밥 뭐 먹지… +"notification.dailyWeekdayNotificationTitle" = "🤔 오늘 밥 뭐 먹지…"; +/// 오늘의 학식을 확인해보세요! +"notification.dailyWeekdayNotificationBody" = "오늘의 학식을 확인해보세요!"; +/// 알림 권한 필요 +"notification_error_permission_denied_message" = "알림 권한 필요"; +/// 알림을 받으려면 설정에서 알림 권한을 허용해주세요. +"notification_error_permission_denied_description" = "알림을 받으려면 설정에서 알림 권한을 허용해주세요."; +/// 알 수 없는 오류 +"notification_error_unknown_message" = "알 수 없는 오류"; +/// 다시 시도해주세요. +"notification_error_unknown_description" = "다시 시도해주세요."; + + +// MARK: - Restaurant + +/// "기숙사 식당" +"restaurant.dormitoryRestaurant" = "기숙사 식당"; +/// "도담 식당" +"restaurant.dodamRestaurant" = "도담 식당"; +/// "학생 식당" +"restaurant.studentRestaurant" = "학생 식당"; +/// "스낵 코너" +"restaurant.snackCorner" = "스낵 코너"; +/// "FACULTY (교직원 전용)" +"restaurant.facultyRestaurant" = "FACULTY (교직원 전용)"; diff --git a/EATSSU/App/Sources/Notification/NotificationManager.swift b/EATSSU/App/Sources/Notification/NotificationManager.swift index bc585862..aee3b253 100644 --- a/EATSSU/App/Sources/Notification/NotificationManager.swift +++ b/EATSSU/App/Sources/Notification/NotificationManager.swift @@ -119,25 +119,26 @@ class NotificationManager { } // MARK: - Error Types + enum NotificationError: Error { case permissionDenied case unknown - + var message: String { switch self { case .permissionDenied: - return "알림 권한 필요" + return TextLiteral.Notification.permissionDeniedMessage case .unknown: - return "알 수 없는 오류" + return TextLiteral.Notification.unknownErrorMessage } } - + var description: String { switch self { case .permissionDenied: - return "알림을 받으려면 설정에서 알림 권한을 허용해주세요." + return TextLiteral.Notification.permissionDeniedDescription case .unknown: - return "다시 시도해주세요." + return TextLiteral.Notification.unknownErrorDescription } } } diff --git a/EATSSU/App/Sources/Presentation/Auth/View/LoginView.swift b/EATSSU/App/Sources/Presentation/Auth/View/LoginView.swift index 7ff3b734..401a7a49 100644 --- a/EATSSU/App/Sources/Presentation/Auth/View/LoginView.swift +++ b/EATSSU/App/Sources/Presentation/Auth/View/LoginView.swift @@ -19,31 +19,30 @@ final class LoginView: BaseUIView { imageView.image = EATSSUDesignAsset.Images.authLogo.image return imageView }() - - private let logoSubTitle: UIImageView = { - let imageView = UIImageView() - imageView.image = EATSSUDesignAsset.Images.authSubTitle.image - return imageView - }() - - let appleLoginButton: UIButton = { - let button = UIButton() - button.setImage(EATSSUDesignAsset.Images.appleLoginButton.image, for: .normal) - return button - }() - - let kakaoLoginButton: UIButton = { - let button = UIButton() - button.setImage(EATSSUDesignAsset.Images.kakaoLoginButton.image, for: .normal) - return button + + private let logoSubTitle: UILabel = { + let label = UILabel() + label.font = .header2 + label.attributedText = TextLiteral.Common.logoSubTitle.logoHighlightedLastWord( + baseColor: .black, + highlightColor: .primary + ) + return label }() - + + let appleLoginButton = SocialLoginButton(type: .apple) + + let kakaoLoginButton = SocialLoginButton(type: .kakao) + let lookingWithNoSignInButton: UIButton = { - let button = UIButton() - button.setImage(EATSSUDesignAsset.Images.lookAroundButton.image, for: .normal) + let button = UIButton(type: .system) + button.setTitle(TextLiteral.Auth.lookingWithNoSignIn, for: .normal) + button.setTitleColor(.gray400, for: .normal) + button.titleLabel?.font = .body2 + button.backgroundColor = .clear return button }() - + private var lastLoginTooltipView: LastLoginTooltipView? override func configureUI() { @@ -69,11 +68,13 @@ final class LoginView: BaseUIView { appleLoginButton.snp.makeConstraints { $0.centerX.equalToSuperview() + $0.horizontalEdges.equalToSuperview().inset(45) $0.bottom.equalTo(self.safeAreaLayoutGuide).inset(151) } kakaoLoginButton.snp.makeConstraints { $0.centerX.equalToSuperview() + $0.horizontalEdges.equalToSuperview().inset(45) $0.bottom.equalTo(self.safeAreaLayoutGuide).inset(90) } diff --git a/EATSSU/App/Sources/Presentation/MyPage/Enum/MyPageLabels.swift b/EATSSU/App/Sources/Presentation/MyPage/Enum/MyPageLabels.swift index eda3f837..e59f8474 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/Enum/MyPageLabels.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/Enum/MyPageLabels.swift @@ -7,29 +7,65 @@ import Foundation -/// "마이파이지"에서 확인할 수 있는 서비스 리스트 -enum MyPageLabels: Int { - /// 푸시 알림 설정 - case NotificationSetting = 0 - - /// 내 정보 - case MyInfo +/// "마이페이지"에서 확인할 수 있는 서비스 리스트 +enum MyPageLabels { + case notificationSetting + case myInfo + case myReview + case inquiry + case creators + case instagram + case languageSetting + case termsAndPolicy + case logout - /// 내 리뷰 - case MyReview - - /// 문의하기 - case Inquiry - - /// 서비스 이용약관 - case TermsOfUse - - /// 개인정보 이용약관 - case PrivacyTermsOfUse - - /// 만든사람들 - case Creator - - /// 로그아웃 - case Logout + var title: String { + switch self { + case .notificationSetting: + return TextLiteral.MyPage.pushNotificationSetting + case .myInfo: + return TextLiteral.MyPage.myInfo + case .myReview: + return TextLiteral.MyPage.myReview + case .inquiry: + return TextLiteral.MyPage.inquiry + case .creators: + return TextLiteral.MyPage.creators + case .instagram: + return TextLiteral.MyPage.instagram + case .languageSetting: + return TextLiteral.MyPage.languageSetting + case .termsAndPolicy: + return TextLiteral.MyPage.termsAndPolicy + case .logout: + return TextLiteral.MyPage.logout + } + } + + var subtitle: String? { + switch self { + case .notificationSetting: + return TextLiteral.MyPage.pushNotificationDescription + default: + return nil + } + } + + var rightText: String? { + switch self { + case .languageSetting: + return TextLiteral.MyPage.currentLanguage + default: + return nil + } + } + + var showsDisclosure: Bool { + switch self { + case .notificationSetting, .logout: + return false + default: + return true + } + } } diff --git a/EATSSU/App/Sources/Presentation/MyPage/Enum/MyPageTableMetric.swift b/EATSSU/App/Sources/Presentation/MyPage/Enum/MyPageTableMetric.swift new file mode 100644 index 00000000..b412f414 --- /dev/null +++ b/EATSSU/App/Sources/Presentation/MyPage/Enum/MyPageTableMetric.swift @@ -0,0 +1,25 @@ +// +// MyPageTableMetric.swift +// EATSSU +// +// Created by jeongminji on 5/3/26. +// + +import Foundation + +enum MyPageTableMetric { + static let normalRowHeight: CGFloat = 48 + static let notificationRowHeight: CGFloat = 74 + static let headerHeight: CGFloat = 18 + static let footerHeight: CGFloat = 16 + + static func rowHeight(for item: MyPageLabels) -> CGFloat { + switch item { + case .notificationSetting: + return notificationRowHeight + + default: + return normalRowHeight + } + } +} diff --git a/EATSSU/App/Sources/Presentation/MyPage/Model/MyPageLocalData.swift b/EATSSU/App/Sources/Presentation/MyPage/Model/MyPageLocalData.swift deleted file mode 100644 index 77010e88..00000000 --- a/EATSSU/App/Sources/Presentation/MyPage/Model/MyPageLocalData.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// MyPageLocalData.swift -// EATSSU_MVC -// -// Created by Jiwoong CHOI on 9/19/24. -// - -import Foundation - -// TODO: 구조체에 익스텐션으로 코드를 작성하는 이유에 대해서 알아보기 - -struct MyPageLocalData { - let titleLabel: String -} - -extension MyPageLocalData { - static let myPageTableLabelList = [ - // "푸시 알림 설정" - MyPageLocalData(titleLabel: TextLiteral.MyPage.pushNotificationSetting), - - // "내 정보" - MyPageLocalData(titleLabel: TextLiteral.MyPage.myInfo), - - // "내 리뷰" - MyPageLocalData(titleLabel: TextLiteral.MyPage.myReview), - - // "문의하기" - MyPageLocalData(titleLabel: TextLiteral.MyPage.inquiry), - - // "서비스 이용약관" - MyPageLocalData(titleLabel: TextLiteral.MyPage.termsOfUse), - - // "개인정보 이용약관" - MyPageLocalData(titleLabel: TextLiteral.MyPage.privacyTermsOfUse), - - // "만든 사람들" - MyPageLocalData(titleLabel: TextLiteral.MyPage.creators), - - // "로그아웃" - MyPageLocalData(titleLabel: TextLiteral.MyPage.logout), - ] -} diff --git a/EATSSU/App/Sources/Presentation/MyPage/Model/MyPageRightItemData.swift b/EATSSU/App/Sources/Presentation/MyPage/Model/MyPageRightItemData.swift deleted file mode 100644 index 7bf44c29..00000000 --- a/EATSSU/App/Sources/Presentation/MyPage/Model/MyPageRightItemData.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// MyPageRightItemData.swift -// EATSSU_MVC -// -// Created by Jiwoong CHOI on 9/19/24. -// - -import Foundation - -/// "마이페이지"에서 사용하는 셀의 오른쪽 데이터 -enum MyPageRightItemData { - /// 앱의 배포 버전 - static var version: String? { - if let info = Bundle.main.infoDictionary, let version = info["CFBundleShortVersionString"] as? String { - version - } else { - nil - } - } -} diff --git a/EATSSU/App/Sources/Presentation/MyPage/Model/MyPageSectionData.swift b/EATSSU/App/Sources/Presentation/MyPage/Model/MyPageSectionData.swift new file mode 100644 index 00000000..87507008 --- /dev/null +++ b/EATSSU/App/Sources/Presentation/MyPage/Model/MyPageSectionData.swift @@ -0,0 +1,44 @@ +// +// MyPageSectionData.swift +// EATSSU +// +// Created by jeongminji on 5/3/26. +// + +import Foundation + +struct MyPageSectionData { + let headerTitle: String + let items: [MyPageLabels] +} + +extension MyPageSectionData { + static var sections: [MyPageSectionData] { + [ + MyPageSectionData( + headerTitle: TextLiteral.MyPage.activitySection, + items: [ + .notificationSetting, + .myInfo, + .myReview + ] + ), + MyPageSectionData( + headerTitle: TextLiteral.MyPage.serviceInfoSection, + items: [ + .inquiry, + .creators, + .instagram + ] + ), + MyPageSectionData( + headerTitle: TextLiteral.MyPage.etcSection, + items: [ + .languageSetting, + .termsAndPolicy, + .logout + ] + ) + ] + } +} diff --git a/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/Cell/MyPageTableDefaultCell.swift b/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/Cell/MyPageTableDefaultCell.swift index a1de498b..e2e9a7c0 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/Cell/MyPageTableDefaultCell.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/Cell/MyPageTableDefaultCell.swift @@ -13,56 +13,109 @@ import EATSSUDesign final class MyPageTableDefaultCell: UITableViewCell { // MARK: - Properties - + static let identifier = "MyPageTableDefaultCell" - + // MARK: - UI Components - + let serviceLabel: UILabel = { let label = UILabel() label.font = .body1 - label.textColor = .gray700Basic + label.textColor = .black return label }() - + + private let rightTextLabel: UILabel = { + let label = UILabel() + label.font = .body2 + label.textColor = .gray600 + label.textAlignment = .right + return label + }() + let rigthChevronImage: UIImageView = { let imageView = UIImageView() imageView.image = UIImage(systemName: "chevron.right") imageView.tintColor = .gray300 return imageView }() - + // MARK: - Initializer - + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - + configureUI() setLayout() } - + @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Functions - + private func configureUI() { addSubviews( serviceLabel, + rightTextLabel, rigthChevronImage ) } - + private func setLayout() { serviceLabel.snp.makeConstraints { $0.leading.equalToSuperview().offset(24) $0.centerY.equalToSuperview() } + rightTextLabel.snp.makeConstraints { + $0.trailing.equalToSuperview().inset(46) + $0.centerY.equalToSuperview() + } rigthChevronImage.snp.makeConstraints { $0.trailing.equalToSuperview().inset(24) $0.centerY.equalToSuperview() } } + + /// 마이페이지 메뉴 셀 설정 + /// - MyPageLabels enum을 그대로 받아서 title, rightText, disclosure 표시 여부를 설정합니다. + /// - MyPageViewController에서 사용하는 기본 설정 함수입니다. + func configure(with item: MyPageLabels) { + serviceLabel.text = item.title + + if let rightText = item.rightText { + rightTextLabel.text = rightText + rightTextLabel.isHidden = false + } else { + rightTextLabel.text = nil + rightTextLabel.isHidden = true + } + + rigthChevronImage.isHidden = !item.showsDisclosure + } + + /// 일반 텍스트 기반 셀 설정 + /// - Parameters: + /// - title: 왼쪽에 표시할 메인 텍스트 + /// - rightText: 오른쪽에 표시할 보조 텍스트. nil이면 숨김 처리됩니다. + /// - showsDisclosure: 오른쪽 chevron 표시 여부 + func configure( + title: String, + rightText: String? = nil, + showsDisclosure: Bool = true + ) { + serviceLabel.text = title + + if let rightText { + rightTextLabel.text = rightText + rightTextLabel.isHidden = false + } else { + rightTextLabel.text = nil + rightTextLabel.isHidden = true + } + + rigthChevronImage.isHidden = !showsDisclosure + } } diff --git a/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/Cell/NotificationSettingTableViewCell.swift b/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/Cell/NotificationSettingTableViewCell.swift index 68bb8391..b2369e4d 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/Cell/NotificationSettingTableViewCell.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/Cell/NotificationSettingTableViewCell.swift @@ -11,7 +11,7 @@ import SnapKit import EATSSUDesign -class NotificationSettingTableViewCell: UITableViewCell { +final class NotificationSettingTableViewCell: UITableViewCell { // MARK: - Properties static let identifier = "NotificationSettingTableViewCell" @@ -87,4 +87,9 @@ class NotificationSettingTableViewCell: UITableViewCell { make.centerY.equalToSuperview() } } + + func configure(with item: MyPageLabels) { + pushNotificationTitleLabel.text = item.title + dailyNotificationInfoLabel.text = item.subtitle + } } diff --git a/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/Cell/RadioSelectionTableViewCell.swift b/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/Cell/RadioSelectionTableViewCell.swift new file mode 100644 index 00000000..cf4afd8d --- /dev/null +++ b/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/Cell/RadioSelectionTableViewCell.swift @@ -0,0 +1,77 @@ +// +// RadioSelectionTableViewCell.swift +// EATSSU +// +// Created by jeongminji on 5/3/26. +// + +import UIKit + +import SnapKit + +import EATSSUDesign + +final class RadioSelectionTableViewCell: UITableViewCell { + // MARK: - Properties + + static let identifier = "RadioSelectionTableViewCell" + + // MARK: - UI Components + + private let titleLabel: UILabel = { + let label = UILabel() + label.font = .body1 + label.textColor = .black + return label + }() + + private let radioButton: CustomRadioButton = { + let button = CustomRadioButton() + button.isUserInteractionEnabled = false + return button + }() + + // MARK: - Initializer + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + configureUI() + setLayout() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Functions + + private func configureUI() { + contentView.addSubviews( + titleLabel, + radioButton + ) + } + + private func setLayout() { + titleLabel.snp.makeConstraints { + $0.leading.equalToSuperview().inset(24) + $0.centerY.equalToSuperview() + } + + radioButton.snp.makeConstraints { + $0.trailing.equalToSuperview().inset(24) + $0.centerY.equalToSuperview() + $0.width.height.equalTo(20) + } + } + + func configure( + title: String, + isSelected: Bool + ) { + titleLabel.text = title + radioButton.updateState(isSelected: isSelected) + } +} diff --git a/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift b/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift index b645b53f..edb29939 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift @@ -12,37 +12,73 @@ import SnapKit import EATSSUDesign final class MyPageView: BaseUIView { - // MARK: - UI Components + // MARK: - Properties + private static var appVersion: String { + guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { + return "-" + } + + return version + } + + private var myPageTableViewHeight: CGFloat { + let rowTotalHeight = MyPageSectionData.sections.reduce(CGFloat(0)) { result, section in + let sectionRowHeight = section.items.reduce(CGFloat(0)) { rowResult, item in + return rowResult + MyPageTableMetric.rowHeight(for: item) + } + + return result + sectionRowHeight + } + let sectionCount = MyPageSectionData.sections.count + let headerTotalHeight = CGFloat(sectionCount) * MyPageTableMetric.headerHeight + + let footerCount = max(sectionCount - 1, 0) + let footerTotalHeight = CGFloat(footerCount) * MyPageTableMetric.footerHeight + + return rowTotalHeight + headerTotalHeight + footerTotalHeight + } + + // MARK: - UI Components + /// MyPageView 전체 스크롤뷰 private let scrollView = UIScrollView() - + /// 스크롤뷰 안에 들어갈 콘텐츠 뷰 private let contentView = UIView() - + // 사용자 이미지 var userImage: UIImageView = { let imageView = UIImageView() imageView.image = EATSSUDesignAsset.Images.profile.image return imageView }() - - // 닉네임이 들어간 닉네임 변경 버튼 + + // 유저 닉네임 var userNicknameLabel: UILabel = { let label = UILabel() label.text = TextLiteral.MyPage.retry label.textColor = .gray700Basic - label.font = .header1 + label.font = .subtitle2 return label }() - + + // 유저 소속 + var userAffiliationLabel: UILabel = { + let label = UILabel() + label.textColor = .gray600 + label.font = .body3 + label.numberOfLines = 1 + return label + }() + let myPageTableView: UITableView = { let tableView = UITableView() tableView.separatorStyle = .none tableView.isScrollEnabled = false return tableView }() - + // "앱 버전" 레이블 private let appVersionStringLabel: UILabel = { let label = UILabel() @@ -51,16 +87,16 @@ final class MyPageView: BaseUIView { label.textColor = .gray400 return label }() - + // 현재 배포된 앱의 버전 private let appVersionLabel: UILabel = { let label = UILabel() - label.text = MyPageRightItemData.version + label.text = appVersion label.font = .caption2 label.textColor = .gray400 return label }() - + /// "탈퇴하기" 레이블과 탈퇴하기 아이콘 let userWithdrawButton: UIButton = { let button = UIButton() @@ -69,33 +105,35 @@ final class MyPageView: BaseUIView { button.setTitleColor(.gray400, for: .normal) button.titleLabel?.font = .caption2 button.tintColor = .red + button.semanticContentAttribute = .forceRightToLeft return button }() - + /// "탈퇴하기" 레이블 underline private let underLineView: UIView = { let view = UIView() view.backgroundColor = .gray400 return view }() - + // MARK: - Intializer - + override init(frame: CGRect) { super.init(frame: frame) - + registerTableViewCells() } - + // MARK: - Functions - + override func configureUI() { addSubview(scrollView) scrollView.addSubview(contentView) - + contentView.addSubviews( userImage, userNicknameLabel, + userAffiliationLabel, myPageTableView, appVersionStringLabel, appVersionLabel, @@ -103,62 +141,64 @@ final class MyPageView: BaseUIView { underLineView ) } - + override func setLayout() { scrollView.snp.makeConstraints { $0.edges.equalToSuperview() } - + contentView.snp.makeConstraints { $0.edges.equalToSuperview() $0.width.equalTo(scrollView) } - + userImage.snp.makeConstraints { - $0.top.equalToSuperview().offset(24) - $0.centerX.equalToSuperview() - $0.height.width.equalTo(100) + $0.top.equalToSuperview().offset(12) + $0.leading.equalToSuperview().inset(24) + $0.height.width.equalTo(48) } - + userNicknameLabel.snp.makeConstraints { - $0.top.equalTo(userImage.snp.bottom).offset(6) - $0.centerX.equalTo(userImage) - $0.height.equalTo(40) + $0.leading.equalTo(userImage.snp.trailing).offset(12) + $0.bottom.equalTo(userImage.snp.centerY) } - + + userAffiliationLabel.snp.makeConstraints { + $0.top.equalTo(userNicknameLabel.snp.bottom) + $0.leading.equalTo(userNicknameLabel.snp.leading) + } + myPageTableView.snp.makeConstraints { - $0.top.equalTo(userNicknameLabel.snp.bottom).offset(16) + $0.top.equalTo(userImage.snp.bottom).offset(24) $0.leading.trailing.equalToSuperview() - let cellHeight = 60 - let totalHeight = MyPageLocalData.myPageTableLabelList.count * cellHeight - $0.height.equalTo(totalHeight) + $0.height.equalTo(myPageTableViewHeight) $0.width.equalToSuperview() } - + appVersionStringLabel.snp.makeConstraints { make in make.top.equalTo(myPageTableView.snp.bottom).offset(6) make.leading.equalToSuperview().inset(24) } - + appVersionLabel.snp.makeConstraints { make in make.top.equalTo(myPageTableView.snp.bottom).offset(6) make.trailing.equalToSuperview().inset(24) } - + // TODO: withdrawStackView를 프로퍼티로 선언할 때, lazy를 사용하면 레이아웃이 한 타임 늦게 잡히는 문제로 인해서 여기에서 스택 안에 들어갈 뷰를 추가함. 개선 방법이 없는지 확인. userWithdrawButton.snp.makeConstraints { make in make.top.equalTo(appVersionLabel.snp.bottom).offset(16) make.trailing.equalToSuperview().inset(24) make.bottom.equalToSuperview().inset(70) } - + underLineView.snp.makeConstraints { $0.top.equalTo(userWithdrawButton.snp.bottom) $0.leading.trailing.equalTo(userWithdrawButton) $0.height.equalTo(0.5) } } - + private func registerTableViewCells() { myPageTableView.register( MyPageTableDefaultCell.self, @@ -169,8 +209,20 @@ final class MyPageView: BaseUIView { forCellReuseIdentifier: NotificationSettingTableViewCell.identifier ) } - - public func setUserInfo(nickname: String) { + + public func setUserInfo( + nickname: String, + collegeName: String?, + departmentName: String? + ) { userNicknameLabel.text = nickname + + let affiliationText = [collegeName, departmentName] + .compactMap { $0 } + .filter { !$0.isEmpty } + .joined(separator: " ") + + userAffiliationLabel.text = affiliationText + userAffiliationLabel.isHidden = affiliationText.isEmpty } } diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/LanguageSettingViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/LanguageSettingViewController.swift new file mode 100644 index 00000000..3c54718d --- /dev/null +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/LanguageSettingViewController.swift @@ -0,0 +1,208 @@ +// +// LanguageSettingViewController.swift +// EATSSU +// +// Created by jeongminji on 5/3/26. +// + +import UIKit + +import SnapKit +import EATSSUDesign + +final class LanguageSettingViewController: BaseViewController { + override var shouldHideTabBar: Bool { true } + + // MARK: - Properties + + /// 언어 설정 화면에서 실제로 언어가 변경되었는지 여부 + /// - true이면 뒤로가기 시 앱 전체 화면을 새 언어 기준으로 다시 구성 + private var didChangeLanguage = false + + /// 버튼 뒤로가기와 swipe back에서 root 재구성이 중복 호출되는 것을 방지 + private var isResettingRootViewController = false + + private var selectedLanguage: AppLanguage { + return AppLanguageManager.shared.currentLanguage + } + + // MARK: - UI Components + + private let tableView: UITableView = { + let tableView = UITableView() + tableView.separatorStyle = .none + tableView.rowHeight = 48 + tableView.backgroundColor = .white + return tableView + }() + + // MARK: - Life Cycles + + override func viewDidLoad() { + super.viewDidLoad() + + setTableViewDelegate() + registerTableViewCells() + setInteractivePopGesture() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + guard didChangeLanguage, + isMovingFromParent, + !isResettingRootViewController else { + return + } + + resetRootAfterLanguageChangeIfNeeded() + } + + // MARK: - Functions + + override func setCustomNavigationBar() { + super.setCustomNavigationBar() + + navigationItem.title = TextLiteral.MyPage.languageSetting + + let backButton = UIBarButtonItem( + image: UIImage(systemName: "chevron.left"), + style: .plain, + target: self, + action: #selector(backButtonDidTap) + ) + + backButton.tintColor = .gray500 + navigationItem.leftBarButtonItem = backButton + } + + override func configureUI() { + view.backgroundColor = .white + view.addSubview(tableView) + } + + override func setLayout() { + tableView.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.leading.trailing.bottom.equalToSuperview() + } + } + + private func setTableViewDelegate() { + tableView.dataSource = self + tableView.delegate = self + } + + private func registerTableViewCells() { + tableView.register( + RadioSelectionTableViewCell.self, + forCellReuseIdentifier: RadioSelectionTableViewCell.identifier + ) + } + + private func setInteractivePopGesture() { + navigationController?.interactivePopGestureRecognizer?.delegate = self + navigationController?.interactivePopGestureRecognizer?.isEnabled = true + } + + @objc + private func backButtonDidTap() { + if didChangeLanguage { + resetRootAfterLanguageChangeIfNeeded() + } else { + navigationController?.popViewController(animated: true) + } + } + + private func changeLanguage(to language: AppLanguage) { + guard language != AppLanguageManager.shared.currentLanguage else { + return + } + + AppLanguageManager.shared.changeLanguage(to: language) + didChangeLanguage = true + + updateLocalizedTexts() + } + + private func updateLocalizedTexts() { + navigationItem.title = TextLiteral.MyPage.languageSetting + tableView.reloadData() + } + + private func resetRootAfterLanguageChangeIfNeeded() { + guard !isResettingRootViewController else { return } + + isResettingRootViewController = true + resetRootViewController() + } + + private func resetRootViewController() { + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) else { + return + } + + let customTabBarController = CustomTabBarContainerController() + + _ = customTabBarController.view + customTabBarController.setTab(index: 3) + + keyWindow.replaceRootViewController(customTabBarController) + } +} + +// MARK: - UITableViewDataSource + +extension LanguageSettingViewController: UITableViewDataSource { + func tableView( + _ tableView: UITableView, + numberOfRowsInSection section: Int + ) -> Int { + return AppLanguage.allCases.count + } + + func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell( + withIdentifier: RadioSelectionTableViewCell.identifier, + for: indexPath + ) as? RadioSelectionTableViewCell else { + return UITableViewCell() + } + + let language = AppLanguage.allCases[indexPath.row] + + cell.configure( + title: language.title, + isSelected: language == selectedLanguage + ) + + return cell + } +} + +// MARK: - UITableViewDelegate + +extension LanguageSettingViewController: UITableViewDelegate { + func tableView( + _ tableView: UITableView, + didSelectRowAt indexPath: IndexPath + ) { + tableView.deselectRow(at: indexPath, animated: true) + + let language = AppLanguage.allCases[indexPath.row] + + changeLanguage(to: language) + } +} + +// MARK: - UIGestureRecognizerDelegate + +extension LanguageSettingViewController: UIGestureRecognizerDelegate { + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return navigationController?.viewControllers.count ?? 0 > 1 + } +} diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift index d93632d2..ffe96a55 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift @@ -16,20 +16,23 @@ import SnapKit final class MyPageViewController: BaseViewController { // MARK: - Properties - + private enum URLConstants { + static let instagram = "https://www.instagram.com/eatssu.official/" + } + private var nickName = "" private var switchState = false - private let myPageTableLabelList = MyPageLocalData.myPageTableLabelList - + private let sections = MyPageSectionData.sections + // MARK: - UI Components - + let mypageView = MyPageView() - + // MARK: - Life Cycles - + override func viewDidLoad() { super.viewDidLoad() - + setTableViewDelegate() loadSwitchStateFromUserDefaults() } @@ -39,36 +42,43 @@ final class MyPageViewController: BaseViewController { logScreenView(screenID: FirebaseScreenID.MyPage.mypage1) } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + + let userInfo = UserInfoManager.shared.getCurrentUserInfo() - nickName = UserInfoManager.shared.getCurrentUserInfo()?.nickname ?? TextLiteral.MyPage.unknownUser - mypageView.setUserInfo(nickname: nickName) - } + nickName = userInfo?.nickname ?? TextLiteral.MyPage.unknownUser + mypageView.setUserInfo( + nickname: nickName, + collegeName: userInfo?.collegeName, + departmentName: userInfo?.departmentName + ) + } + // MARK: - Functions - + override func setCustomNavigationBar() { super.setCustomNavigationBar() navigationItem.title = TextLiteral.MyPage.myPage } - + override func configureUI() { view.addSubviews(mypageView) } - + override func setLayout() { mypageView.snp.makeConstraints { $0.edges.equalToSuperview() } } - + override func setButtonEvent() { mypageView.userWithdrawButton .addTarget(self, action: #selector(userWithdrawButtonTapped), for: .touchUpInside) } - + private func setFirebaseTask() { FirebaseRemoteConfig.shared.fetchRestaurantInfo() } @@ -79,70 +89,95 @@ final class MyPageViewController: BaseViewController { let userWithdrawViewController = UserWithdrawViewController(nickName: nickName) navigationController?.pushViewController(userWithdrawViewController, animated: true) } - + /// TableViewDelegate & DataSource를 해당 클래스로 할당합니다. private func setTableViewDelegate() { mypageView.myPageTableView.dataSource = self mypageView.myPageTableView.delegate = self + + if #available(iOS 15.0, *) { + mypageView.myPageTableView.sectionHeaderTopPadding = 0 + } } - + /// 로그아웃 Alert를 스크린에 표시하는 메소드 private func logoutShowAlert() { let alert = UIAlertController(title: TextLiteral.MyPage.logout, message: TextLiteral.MyPage.askLogout, preferredStyle: UIAlertController.Style.alert) - + let cancelAction = UIAlertAction(title: TextLiteral.Common.cancelDark, style: .default, handler: nil) - + let fixAction = UIAlertAction(title: TextLiteral.MyPage.logout, style: .default, handler: { _ in - RealmService.shared.resetDB() - - let loginViewController = LoginViewController() - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) - { - keyWindow.replaceRootViewController(UINavigationController(rootViewController: loginViewController)) - } - }) - + RealmService.shared.resetDB() + + let loginViewController = LoginViewController() + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) + { + keyWindow.replaceRootViewController(UINavigationController(rootViewController: loginViewController)) + } + }) + alert.addAction(cancelAction) alert.addAction(fixAction) - + present(alert, animated: true, completion: nil) } - + /// UserDefaults에 스위치 상태 저장 private func saveSwitchStateToUserDefaults() { print("사용자 푸시 알림 값을 앱 저장소에 보관합니다.") UserDefaults.standard.set(switchState, forKey: TextLiteral.MyPage.pushNotificationUserSettingKey) } - + /// UserDefaults에서 스위치 상태 불러오기 private func loadSwitchStateFromUserDefaults() { print("사용자 푸시 알림 값을 앱 저장소에서 불러옵니다.") switchState = UserDefaults.standard.bool(forKey: TextLiteral.MyPage.pushNotificationUserSettingKey) } + + /// indexPath로 현재 item을 가져오기 + private func item(at indexPath: IndexPath) -> MyPageLabels { + return sections[indexPath.section].items[indexPath.row] + } } // MARK: - TableView DataSource extension MyPageViewController: UITableViewDataSource { - func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { - myPageTableLabelList.count + func numberOfSections(in tableView: UITableView) -> Int { + return sections.count } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if indexPath.row == MyPageLabels.NotificationSetting.rawValue { - let cell = tableView - .dequeueReusableCell( - withIdentifier: NotificationSettingTableViewCell.identifier, - for: indexPath - ) as! NotificationSettingTableViewCell + + func tableView( + _ tableView: UITableView, + numberOfRowsInSection section: Int + ) -> Int { + return sections[section].items.count + } + + func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { + let item = item(at: indexPath) + + switch item { + case .notificationSetting: + guard let cell = tableView.dequeueReusableCell( + withIdentifier: NotificationSettingTableViewCell.identifier, + for: indexPath + ) as? NotificationSettingTableViewCell else { + return UITableViewCell() + } + + cell.configure(with: item) - // Task로 비동기 작업 처리 _Concurrency.Task { let settings = await NotificationManager.shared.checkNotificationSetting() @@ -157,16 +192,19 @@ extension MyPageViewController: UITableViewDataSource { } } } + return cell - } else { - let cell = tableView - .dequeueReusableCell( - withIdentifier: MyPageTableDefaultCell.identifier, - for: indexPath - ) as! MyPageTableDefaultCell - let title = myPageTableLabelList[indexPath.row].titleLabel - cell.serviceLabel.text = title + default: + guard let cell = tableView.dequeueReusableCell( + withIdentifier: MyPageTableDefaultCell.identifier, + for: indexPath + ) as? MyPageTableDefaultCell else { + return UITableViewCell() + } + + cell.configure(with: item) + return cell } } @@ -175,34 +213,72 @@ extension MyPageViewController: UITableViewDataSource { // MARK: - UITableView Delegate extension MyPageViewController: UITableViewDelegate { - func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat { - 60 + func tableView( + _ tableView: UITableView, + heightForRowAt indexPath: IndexPath + ) -> CGFloat { + let item = item(at: indexPath) + return MyPageTableMetric.rowHeight(for: item) } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + + func tableView( + _ tableView: UITableView, + heightForHeaderInSection section: Int + ) -> CGFloat { + return MyPageTableMetric.headerHeight + } + + func tableView( + _ tableView: UITableView, + viewForHeaderInSection section: Int + ) -> UIView? { + let headerView = UIView() + headerView.backgroundColor = .white + + let titleLabel = UILabel() + titleLabel.text = sections[section].headerTitle + titleLabel.font = .caption1 + titleLabel.textColor = .gray500 + + headerView.addSubview(titleLabel) + + titleLabel.snp.makeConstraints { + $0.leading.equalToSuperview().inset(24) + $0.centerY.equalToSuperview() + } + + return headerView + } + + func tableView( + _ tableView: UITableView, + didSelectRowAt indexPath: IndexPath + ) { tableView.deselectRow(at: indexPath, animated: true) - - switch indexPath.row { - // "푸시 알림 설정" 스위치 토글 - case MyPageLabels.NotificationSetting.rawValue: + + let item = item(at: indexPath) + + switch item { + // "푸시 알림 설정" 스위치 토글 + case .notificationSetting: AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "notification_setting"]) handleNotificationSettingToggle(at: indexPath) - - // "내 정보" 스크린으로 이동 - case MyPageLabels.MyInfo.rawValue: + + // "내 정보" 스크린으로 이동 + case .myInfo: AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "my_info"]) let setNickNameVC = SetNickNameViewController() setNickNameVC.source = .signup navigationController?.pushViewController(setNickNameVC, animated: true) - - // "내 리뷰" 스크린으로 이동 - case MyPageLabels.MyReview.rawValue: + + // "내 리뷰" 스크린으로 이동 + case .myReview: AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "my_review"]) let myReviewViewController = MyReviewViewController(nickname: nickName) navigationController?.pushViewController(myReviewViewController, animated: true) - - // "문의하기" 스크린으로 이동 - case MyPageLabels.Inquiry.rawValue: + + // "문의하기" 스크린으로 이동 + case .inquiry: AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "inquiry"]) TalkApi.shared.chatChannel(channelPublicId: TextLiteral.KakaoChannel.id) { [weak self] error in if error != nil { @@ -219,34 +295,69 @@ extension MyPageViewController: UITableViewDelegate { // TODO: 카카오톡 채널 채팅방으로 연결 성공했을 때, 앱에서 동작되어야 하는 로직 고민 } } - - // "서비스 이용약관" 스크린으로 이동 - case MyPageLabels.TermsOfUse.rawValue: - AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "terms_of_use"]) - let provisionViewController = ProvisionViewController(agreementType: .termsOfService) - provisionViewController.navigationTitle = TextLiteral.MyPage.termsOfUse - navigationController?.pushViewController(provisionViewController, animated: true) - - // "개인정보 이용약관" 스크린으로 이동 - case MyPageLabels.PrivacyTermsOfUse.rawValue: - AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "privacy_policy"]) - let provisionViewController = ProvisionViewController(agreementType: .privacyPolicy) - provisionViewController.navigationTitle = TextLiteral.MyPage.privacyTermsOfUse - navigationController?.pushViewController(provisionViewController, animated: true) - + // "만든사람들" 스크린으로 이동 - case MyPageLabels.Creator.rawValue: + case .creators: AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "creator"]) let creatorViewController = CreatorViewController() navigationController?.pushViewController(creatorViewController, animated: true) + + // 잇슈 인스타그램 이동 + case .instagram: + // TODO: 실제 로그 이름 통일 + //AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "instagram"]) + + if let instagramURL = URL(string: URLConstants.instagram) { + UIApplication.shared.open(instagramURL) + } + + case .languageSetting: + // TODO: 실제 로그 이름 통일 + // AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "language_setting"]) - // "로그아웃" 팝업알림 표시 - case MyPageLabels.Logout.rawValue: + let languageSettingViewController = LanguageSettingViewController() + navigationController?.pushViewController(languageSettingViewController, animated: true) + + // "약관 및 정책" 스크린으로 이동 + case .termsAndPolicy: + let termsAndPolicyViewController = TermsAndPolicyViewController() + navigationController?.pushViewController(termsAndPolicyViewController, animated: true) + + // "로그아웃" 팝업알림 표시 + case .logout: logoutShowAlert() + } + } + + func tableView( + _ tableView: UITableView, + heightForFooterInSection section: Int + ) -> CGFloat { + return section == sections.count - 1 ? CGFloat.leastNormalMagnitude : MyPageTableMetric.footerHeight + } - default: - return + func tableView( + _ tableView: UITableView, + viewForFooterInSection section: Int + ) -> UIView? { + guard section != sections.count - 1 else { + return nil } + + let footerView = UIView() + + let dividerView = UIView() + dividerView.backgroundColor = .gray300 + + footerView.addSubview(dividerView) + + dividerView.snp.makeConstraints { + $0.height.equalTo(1) + $0.horizontalEdges.equalToSuperview() + $0.centerY.equalToSuperview() + } + + return footerView } /// 알림 설정 토글 처리 @@ -272,8 +383,8 @@ extension MyPageViewController: UITableViewDelegate { let formattedDate = dateFormatter.string(from: Date()) let message = newState - ? TextLiteral.MyPage.agreeNoti(date: formattedDate) - : TextLiteral.MyPage.disagreeNoti(date: formattedDate) + ? TextLiteral.MyPage.agreeNoti(date: formattedDate) + : TextLiteral.MyPage.disagreeNoti(date: formattedDate) self.showToast(message: message, type: .info) } @@ -302,7 +413,7 @@ extension MyPageViewController: UITableViewDelegate { ) let settingsAction = UIAlertAction(title: TextLiteral.Common.moveToSetting, style: .default) { _ in - NotificationManager.shared.openNotificationSettings() + NotificationManager.shared.openNotificationSettings() } let cancelAction = UIAlertAction(title: TextLiteral.Common.cancel, style: .cancel) diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/TermsAndPolicyViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/TermsAndPolicyViewController.swift new file mode 100644 index 00000000..06719e48 --- /dev/null +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/TermsAndPolicyViewController.swift @@ -0,0 +1,144 @@ +// +// TermsAndPolicyViewController.swift +// EATSSU +// +// Created by jeongminji on 5/3/26. +// + +import UIKit + +import SnapKit + +final class TermsAndPolicyViewController: BaseViewController { + override var shouldHideTabBar: Bool { true } + // MARK: - Properties + + enum TermsAndPolicyType: CaseIterable { + case termsOfUse + case privacyTermsOfUse + + var title: String { + switch self { + case .termsOfUse: + return TextLiteral.MyPage.termsOfUse + + case .privacyTermsOfUse: + return TextLiteral.MyPage.privacyTermsOfUse + } + } + } + + private let termsAndPolicyItems = TermsAndPolicyType.allCases + + // MARK: - UI Components + + private let tableView: UITableView = { + let tableView = UITableView() + tableView.separatorStyle = .none + tableView.rowHeight = 48 + tableView.backgroundColor = .white + return tableView + }() + + // MARK: - Life Cycles + + override func viewDidLoad() { + super.viewDidLoad() + + setTableViewDelegate() + registerTableViewCells() + } + + // MARK: - Functions + + override func setCustomNavigationBar() { + super.setCustomNavigationBar() + + navigationItem.title = TextLiteral.MyPage.termsAndPolicy + } + + override func configureUI() { + view.backgroundColor = .white + view.addSubview(tableView) + } + + override func setLayout() { + tableView.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.leading.trailing.bottom.equalToSuperview() + } + } + + private func setTableViewDelegate() { + tableView.dataSource = self + tableView.delegate = self + } + + private func registerTableViewCells() { + tableView.register( + MyPageTableDefaultCell.self, + forCellReuseIdentifier: MyPageTableDefaultCell.identifier + ) + } + + private func pushProvisionViewController(with item: TermsAndPolicyType) { + let provisionViewController: ProvisionViewController + + switch item { + case .termsOfUse: + AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "terms_of_use"]) + provisionViewController = ProvisionViewController(agreementType: .termsOfService) + + case .privacyTermsOfUse: + AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "privacy_policy"]) + provisionViewController = ProvisionViewController(agreementType: .privacyPolicy) + } + + provisionViewController.navigationTitle = item.title + + navigationController?.pushViewController( + provisionViewController, + animated: true + ) + } +} + +// MARK: - UITableViewDataSource +extension TermsAndPolicyViewController: UITableViewDataSource { + func tableView( + _ tableView: UITableView, + numberOfRowsInSection section: Int + ) -> Int { + return termsAndPolicyItems.count + } + + func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell( + withIdentifier: MyPageTableDefaultCell.identifier, + for: indexPath + ) as? MyPageTableDefaultCell else { + return UITableViewCell() + } + + let item = termsAndPolicyItems[indexPath.row] + cell.configure(title: item.title) + + return cell + } +} + +// MARK: - UITableViewDelegate +extension TermsAndPolicyViewController: UITableViewDelegate { + func tableView( + _ tableView: UITableView, + didSelectRowAt indexPath: IndexPath + ) { + tableView.deselectRow(at: indexPath, animated: true) + + let item = termsAndPolicyItems[indexPath.row] + pushProvisionViewController(with: item) + } +} diff --git a/EATSSU/App/Sources/Utility/Extension/String+.swift b/EATSSU/App/Sources/Utility/Extension/String+.swift new file mode 100644 index 00000000..dce0e050 --- /dev/null +++ b/EATSSU/App/Sources/Utility/Extension/String+.swift @@ -0,0 +1,44 @@ +// +// String+.swift +// EATSSU +// +// Created by jeongminji on 5/4/26. +// + +import Foundation + +import UIKit + +extension String { + /// 문자열에서 마지막 단어만 지정한 색상으로 강조한 attributed string을 반환 + /// - Parameters: + /// - baseColor: 전체 문자열에 기본으로 적용할 색상 + /// - highlightColor: 마지막 단어에 적용할 강조 색상 + /// - Returns: 마지막 단어만 강조 색상이 적용된 `NSAttributedString` + func logoHighlightedLastWord( + baseColor: UIColor, + highlightColor: UIColor + ) -> NSAttributedString { + let attributedString = NSMutableAttributedString( + string: self, + attributes: [ + .foregroundColor: baseColor + ] + ) + + guard let lastWord = self.split(separator: " ").last else { + return attributedString + } + + let nsString = self as NSString + let range = nsString.range(of: String(lastWord), options: .backwards) + + attributedString.addAttribute( + .foregroundColor, + value: highlightColor, + range: range + ) + + return attributedString + } +} diff --git a/EATSSU/App/Sources/Utility/Literal/AppLanguage.swift b/EATSSU/App/Sources/Utility/Literal/AppLanguage.swift new file mode 100644 index 00000000..3dcc9f6f --- /dev/null +++ b/EATSSU/App/Sources/Utility/Literal/AppLanguage.swift @@ -0,0 +1,22 @@ +// +// AppLanguage.swift +// EATSSU +// +// Created by jeongminji on 5/3/26. +// + +import Foundation + +enum AppLanguage: String, CaseIterable { + case korean = "ko" + case english = "en" + + var title: String { + switch self { + case .korean: + return "한국어" + case .english: + return "English" + } + } +} diff --git a/EATSSU/App/Sources/Utility/Literal/AppLanguageManager.swift b/EATSSU/App/Sources/Utility/Literal/AppLanguageManager.swift new file mode 100644 index 00000000..77d5f1f8 --- /dev/null +++ b/EATSSU/App/Sources/Utility/Literal/AppLanguageManager.swift @@ -0,0 +1,71 @@ +// +// AppLanguageManager.swift +// EATSSU +// +// Created by jeongminji on 5/3/26. +// + +import Foundation + +final class AppLanguageManager { + static let shared = AppLanguageManager() + + private init() {} + + private enum Constant { + /// 사용자가 직접 고른 언어 저장 key + static let selectedLanguageKey = "selectedAppLanguage" + + /// 사용자가 앱에서 직접 언어를 바꾼 적 있는지 저장 key + static let didSelectLanguageManuallyKey = "didSelectLanguageManually" + } + + // MARK: - Current Language + + /// 1순위: 사용자가 직접 선택한 언어가 있으면 그걸 우선 사용 + /// 2순위: 사용자가 직접 선택한 적이 없으면 휴대폰 언어 사용 + var currentLanguage: AppLanguage { + if didSelectLanguageManually, + let savedLanguageCode = UserDefaults.standard.string(forKey: Constant.selectedLanguageKey), + let savedLanguage = AppLanguage(rawValue: savedLanguageCode) { + return savedLanguage + } + + return deviceLanguage + } + + var didSelectLanguageManually: Bool { + return UserDefaults.standard.bool(forKey: Constant.didSelectLanguageManuallyKey) + } + + private var deviceLanguage: AppLanguage { + let preferredCode = Locale.preferredLanguages.first ?? "ko" + + if preferredCode.hasPrefix("en") { + return .english + } else { + return .korean + } + } + + // MARK: - Bundle + + var bundle: Bundle { + guard let path = Bundle.main.path( + forResource: currentLanguage.rawValue, + ofType: "lproj" + ), + let bundle = Bundle(path: path) else { + return .main + } + + return bundle + } + + // MARK: - Change Language + + func changeLanguage(to language: AppLanguage) { + UserDefaults.standard.set(language.rawValue, forKey: Constant.selectedLanguageKey) + UserDefaults.standard.set(true, forKey: Constant.didSelectLanguageManuallyKey) + } +} diff --git a/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift b/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift index 25497efe..767382d0 100644 --- a/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift +++ b/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift @@ -7,9 +7,38 @@ import Foundation -enum TextLiteral { +private enum Localization { + static func localized( + _ key: String, + fallback: String + ) -> String { + let value = AppLanguageManager.shared.bundle.localizedString( + forKey: key, + value: fallback, + table: nil + ) + + return value + } + static func formatted( + _ key: String, + fallback: String, + _ arguments: CVarArg... + ) -> String { + let format = localized(key, fallback: fallback) + + return String( + format: format, + locale: Locale(identifier: AppLanguageManager.shared.currentLanguage.rawValue), + arguments: arguments + ) + } +} + +enum TextLiteral { // MARK: - KakaoChannel + enum KakaoChannel { /// EATSSU 카카오 채널 ID static let id: String = "_ZlVAn" @@ -19,569 +48,944 @@ enum TextLiteral { enum Common { /// "확인" - static let confirm: String = "확인" + static var logoSubTitle: String { + Localization.localized("common.logoSubTitle", fallback: "숭실대에서 먹자") + } + /// "확인" + static var confirm: String { + Localization.localized("common.confirm", fallback: "확인") + } /// "취소" - static let cancel: String = "취소" + static var cancel: String { + Localization.localized("common.cancel", fallback: "취소") + } /// "취소하기" - static let cancelDark: String = "취소하기" + static var cancelDark: String { + Localization.localized("common.cancelDark", fallback: "취소하기") + } /// "삭제하기" - static let delete: String = "삭제하기" + static var delete: String { + Localization.localized("common.delete", fallback: "삭제하기") + } /// "수정하기" - static let fix: String = "수정하기" + static var fix: String { + Localization.localized("common.fix", fallback: "수정하기") + } /// "로그인이 필요한 서비스입니다" - static let needLogin: String = "로그인이 필요한 서비스입니다" + static var needLogin: String { + Localization.localized("common.needLogin", fallback: "로그인이 필요한 서비스입니다") + } /// "로그인 하시겠습니까?" - static let askLogin: String = "로그인 하시겠습니까?" + static var askLogin: String { + Localization.localized("common.askLogin", fallback: "로그인 하시겠습니까?") + } /// "설정으로 이동" - static let moveToSetting: String = "설정으로 이동" + static var moveToSetting: String { + Localization.localized("common.moveToSetting", fallback: "설정으로 이동") + } /// "탈퇴 처리가 완료되었습니다." - static let withdrawComplete: String = "탈퇴 처리가 완료되었습니다." + static var withdrawComplete: String { + Localization.localized("common.withdrawComplete", fallback: "탈퇴 처리가 완료되었습니다.") + } /// "잠시 후 다시 시도해주세요." - static let tryAgain: String = "잠시 후 다시 시도해주세요." + static var tryAgain: String { + Localization.localized("common.tryAgain", fallback: "잠시 후 다시 시도해주세요.") + } /// "세션이 만료되었습니다. 다시 로그인해주세요." - static let sessionExpired: String = "세션이 만료되었습니다. 다시 로그인해주세요." + static var sessionExpired: String { + Localization.localized("common.sessionExpired", fallback: "세션이 만료되었습니다. 다시 로그인해주세요.") + } /// "에러가 발생했습니다" - static let errorOccured: String = "에러가 발생했습니다" + static var errorOccured: String { + Localization.localized("common.errorOccured", fallback: "에러가 발생했습니다") + } /// "다시 시도하세요" - static let retry: String = "다시 시도하세요" + static var retry: String { + Localization.localized("common.retry", fallback: "다시 시도하세요") + } } // MARK: - TabBar enum TabBar { /// "학식" - static let meal: String = "학식" + static var meal: String { + Localization.localized("tabBar.meal", fallback: "학식") + } /// "지도" - static let map: String = "지도" + static var map: String { + Localization.localized("tabBar.map", fallback: "지도") + } /// "나만아니면돼~" - static let coffee: String = "나만아니면돼~" + static var coffee: String { + Localization.localized("tabBar.coffee", fallback: "나만아니면돼~") + } /// "마이" - static let my: String = "마이" + static var my: String { + Localization.localized("tabBar.my", fallback: "마이") + } } // MARK: - Auth enum Auth { /// "닉네임을 입력해주세요" - static let inputNickName: String = "닉네임을 입력해주세요" + static var inputNickName: String { + Localization.localized("auth.inputNickName", fallback: "닉네임을 입력해주세요") + } /// "Apple로 로그인" - static let signInWithApple: String = "Apple로 로그인" + static var signInWithApple: String { + Localization.localized("auth.signInWithApple", fallback: "Apple로 로그인") + } /// "카카오 로그인" - static let signInWithKakao: String = "카카오 로그인" + static var signInWithKakao: String { + Localization.localized("auth.signInWithKakao", fallback: "카카오 로그인") + } /// "둘러보기" - static let lookingWithNoSignIn: String = "둘러보기" + static var lookingWithNoSignIn: String { + Localization.localized("auth.lookingWithNoSignIn", fallback: "둘러보기") + } /// UserDefaults key for last login provider static let lastLoginProviderKey: String = "lastLoginProvider" /// "최근에 로그인했어요" - static let lastLoginTooltip: String = "최근에 로그인했어요" + static var lastLoginTooltip: String { + Localization.localized("auth.lastLoginTooltip", fallback: "최근에 로그인했어요") + } /// LoginVC - "카카오톡으로 생성된 계정입니다." - static let kakaoAccount: String = "카카오톡으로 생성된 계정입니다." + static var kakaoAccount: String { + Localization.localized("auth.kakaoAccount", fallback: "카카오톡으로 생성된 계정입니다.") + } /// LoginVC - "Apple로 생성된 계정입니다." - static let appleAccount: String = "Apple로 생성된 계정입니다." + static var appleAccount: String { + Localization.localized("auth.appleAccount", fallback: "Apple로 생성된 계정입니다.") + } /// SetNickNameView - "닉네임 설정" - static let setNickname: String = "닉네임 설정" + static var setNickname: String { + Localization.localized("auth.setNickname", fallback: "닉네임 설정") + } /// SetNickNameView - "중복 확인" - static let checkDuplicate: String = "중복 확인" + static var checkDuplicate: String { + Localization.localized("auth.checkDuplicate", fallback: "중복 확인") + } /// SetNickNameView - "소속 설정" - static let setCollege: String = "소속 설정" + static var setCollege: String { + Localization.localized("auth.setCollege", fallback: "소속 설정") + } /// SetNickNameView - "단과대" - static let college: String = "단과대" + static var college: String { + Localization.localized("auth.college", fallback: "단과대") + } /// SetNickNameView - "학과" - static let department: String = "학과" + static var department: String { + Localization.localized("auth.department", fallback: "학과") + } /// SetNickNameView - "연결된 계정" - static let linkedAccount: String = "연결된 계정" + static var linkedAccount: String { + Localization.localized("auth.linkedAccount", fallback: "연결된 계정") + } /// SetNickNameView - "없음" - static let empty: String = "없음" + static var empty: String { + Localization.localized("auth.empty", fallback: "없음") + } /// SetNickNameView - "저장하기" - static let save: String = "저장하기" + static var save: String { + Localization.localized("auth.save", fallback: "저장하기") + } /// SetNickNameView - "카카오" - static let kakao: String = "카카오" + static var kakao: String { + Localization.localized("auth.kakao", fallback: "카카오") + } /// SetNickNameView - "APPLE" - static let apple: String = "APPLE" + static var apple: String { + Localization.localized("auth.apple", fallback: "APPLE") + } /// SetNickNameVC - "변경된 정보가 없습니다." - static let noChanges: String = "변경된 정보가 없습니다." + static var noChanges: String { + Localization.localized("auth.noChanges", fallback: "변경된 정보가 없습니다.") + } /// SetNickNameVC - "유효하지 않은 학과 정보입니다." - static let invalidDepartment: String = "유효하지 않은 학과 정보입니다." + static var invalidDepartment: String { + Localization.localized("auth.invalidDepartment", fallback: "유효하지 않은 학과 정보입니다.") + } /// SetNickNameVC - "정보 업데이트 중 오류가 발생했습니다." - static let updateError: String = "정보 업데이트 중 오류가 발생했습니다." + static var updateError: String { + Localization.localized("auth.updateError", fallback: "정보 업데이트 중 오류가 발생했습니다.") + } /// SetNickNameVC - "내 정보가 수정되었어요." - static let updateSuccess: String = "내 정보가 수정되었어요." + static var updateSuccess: String { + Localization.localized("auth.updateSuccess", fallback: "내 정보가 수정되었어요.") + } /// NIcknameTextFieldResultType - "필수 입력 사항입니다" - static let requiredInput: String = "필수 입력 사항입니다" + static var requiredInput: String { + Localization.localized("auth.requiredInput", fallback: "필수 입력 사항입니다") + } /// NIcknameTextFieldResultType - "중복 확인을 진행해주세요." - static let needCheckDuplicate: String = "중복 확인을 진행해주세요." + static var needCheckDuplicate: String { + Localization.localized("auth.needCheckDuplicate", fallback: "중복 확인을 진행해주세요.") + } /// NIcknameTextFieldResultType - "이미 사용 중인 닉네임이에요." - static let duplicatedNickname: String = "이미 사용 중인 닉네임이에요." + static var duplicatedNickname: String { + Localization.localized("auth.duplicatedNickname", fallback: "이미 사용 중인 닉네임이에요.") + } /// NIcknameTextFieldResultType - "사용가능한 닉네임이에요" - static let availableNickname: String = "사용가능한 닉네임이에요" + static var availableNickname: String { + Localization.localized("auth.availableNickname", fallback: "사용가능한 닉네임이에요") + } /// NIcknameTextFieldResultType - "2~16글자를 입력해 주세요." - static let nicknameLength: String = "2~16글자를 입력해 주세요." + static var nicknameLength: String { + Localization.localized("auth.nicknameLength", fallback: "2~16글자를 입력해 주세요.") + } /// NIcknameTextFieldResultType - "특수문자로 시작/끝나는 닉네임은 사용할 수 없어요." - static let specialCharNickname: String = "특수문자로 시작/끝나는 닉네임은 사용할 수 없어요." + static var specialCharNickname: String { + Localization.localized("auth.specialCharNickname", fallback: "특수문자로 시작/끝나는 닉네임은 사용할 수 없어요.") + } /// NIcknameTextFieldResultType - "연속된 특수문자(--, __)는 사용할 수 없어요." - static let continuousSpecialChar: String = "연속된 특수문자(--, __)는 사용할 수 없어요." + static var continuousSpecialChar: String { + Localization.localized("auth.continuousSpecialChar", fallback: "연속된 특수문자(--, __)는 사용할 수 없어요.") + } /// NIcknameTextFieldResultType - "숫자만으로 된 닉네임은 사용할 수 없어요." - static let numberOnlyNickname: String = "숫자만으로 된 닉네임은 사용할 수 없어요." + static var numberOnlyNickname: String { + Localization.localized("auth.numberOnlyNickname", fallback: "숫자만으로 된 닉네임은 사용할 수 없어요.") + } /// NIcknameTextFieldResultType - "허용 문자(한글/영문/숫자)만 사용할 수 있어요." - static let allowedChar: String = "허용 문자(한글/영문/숫자)만 사용할 수 있어요." + static var allowedChar: String { + Localization.localized("auth.allowedChar", fallback: "허용 문자(한글/영문/숫자)만 사용할 수 있어요.") + } /// NIcknameTextFieldResultType - "사용할 수 없는 단어가 포함되어 있어요." - static let bannedWord: String = "사용할 수 없는 단어가 포함되어 있어요." + static var bannedWord: String { + Localization.localized("auth.bannedWord", fallback: "사용할 수 없는 단어가 포함되어 있어요.") + } /// NIcknameTextFieldResultType - "띄어쓰기로 시작/끝나는 닉네임은 사용할 수 없어요." - static let spaceNickname: String = "띄어쓰기로 시작/끝나는 닉네임은 사용할 수 없어요." + static var spaceNickname: String { + Localization.localized("auth.spaceNickname", fallback: "띄어쓰기로 시작/끝나는 닉네임은 사용할 수 없어요.") + } /// NIcknameTextFieldResultType - "연속된 띄어쓰기는 사용할 수 없어요." - static let continuousSpace: String = "연속된 띄어쓰기는 사용할 수 없어요." + static var continuousSpace: String { + Localization.localized("auth.continuousSpace", fallback: "연속된 띄어쓰기는 사용할 수 없어요.") + } /// NIcknameTextFieldResultType - "이모지, 특수문자는 사용할 수 없어요." - static let emojiSpecialChar: String = "이모지, 특수문자는 사용할 수 없어요." + static var emojiSpecialChar: String { + Localization.localized("auth.emojiSpecialChar", fallback: "이모지, 특수문자는 사용할 수 없어요.") + } /// NIcknameTextFieldResultType - "관리자로 혼동될 수 있는 닉네임은 사용할 수 없어요." - static let adminNickname: String = "관리자로 혼동될 수 있는 닉네임은 사용할 수 없어요." + static var adminNickname: String { + Localization.localized("auth.adminNickname", fallback: "관리자로 혼동될 수 있는 닉네임은 사용할 수 없어요.") + } /// NIcknameTextFieldResultType - "서비스명 단독 닉네임은 사용할 수 없어요." - static let serviceNameNickname: String = "서비스명 단독 닉네임은 사용할 수 없어요." + static var serviceNameNickname: String { + Localization.localized("auth.serviceNameNickname", fallback: "서비스명 단독 닉네임은 사용할 수 없어요.") + } /// NIcknameTextFieldResultType - "욕설, 비속어 등의 표현이 포함된 닉네임은 사용할 수 없어요." - static let slangNickname: String = "욕설, 비속어 등의 표현이 포함된 닉네임은 사용할 수 없어요." + static var slangNickname: String { + Localization.localized("auth.slangNickname", fallback: "욕설, 비속어 등의 표현이 포함된 닉네임은 사용할 수 없어요.") + } } // MARK: - Home enum Home { /// Home - "오늘의 메뉴" - static let todayMenu: String = "오늘의 메뉴" + static var todayMenu: String { + Localization.localized("home.todayMenu", fallback: "오늘의 메뉴") + } /// Home - "가격" - static let price: String = "가격" + static var price: String { + Localization.localized("home.price", fallback: "가격") + } /// Home - "평점" - static let rating: String = "평점" + static var rating: String { + Localization.localized("home.rating", fallback: "평점") + } /// Home - " -" - static let emptyRating: String = " -" + static var emptyRating: String { + Localization.localized("home.emptyRating", fallback: " -") + } /// Home - "제공되는 메뉴가 없습니다" - static let noMenuProvidedMessage: String = "제공되는 메뉴가 없습니다" + static var noMenuProvidedMessage: String { + Localization.localized("home.noMenuProvidedMessage", fallback: "제공되는 메뉴가 없습니다") + } /// CustomTimeTabController - "아침" - static let morning: String = "아침" + static var morning: String { + Localization.localized("home.morning", fallback: "아침") + } /// CustomTimeTabController - "점심" - static let lunch: String = "점심" + static var lunch: String { + Localization.localized("home.lunch", fallback: "점심") + } /// CustomTimeTabController - "저녁" - static let dinner: String = "저녁" + static var dinner: String { + Localization.localized("home.dinner", fallback: "저녁") + } /// RestaurantInfoView - "학생 식당" - static let studentRestaurant: String = "학생 식당" + static var studentRestaurant: String { + Localization.localized("home.studentRestaurant", fallback: "학생 식당") + } /// RestaurantInfoView - "식당 위치" - static let restaurantLocation: String = "식당 위치" + static var restaurantLocation: String { + Localization.localized("home.restaurantLocation", fallback: "식당 위치") + } /// RestaurantInfoView - "식당 사진" - static let restaurantPicture: String = "식당 사진" + static var restaurantPicture: String { + Localization.localized("home.restaurantPicture", fallback: "식당 사진") + } /// RestaurantInfoView - "숭실대학교" - static let soongsilUniversity: String = "숭실대학교" + static var soongsilUniversity: String { + Localization.localized("home.soongsilUniversity", fallback: "숭실대학교") + } /// RestaurantInfoView - "영업 시간" - static let businessHour: String = "영업 시간" + static var businessHour: String { + Localization.localized("home.businessHour", fallback: "영업 시간") + } /// RestaurantInfoView - "비고" - static let note: String = "비고" + static var note: String { + Localization.localized("home.note", fallback: "비고") + } /// RestaurantInfoView - "아시안푸드, 돈까스, 샐러드, 국밥 등\n카페" - static let dodamEtc: String = "아시안푸드, 돈까스, 샐러드, 국밥 등\n카페" + static var dodamEtc: String { + Localization.localized("home.dodamEtc", fallback: "아시안푸드, 돈까스, 샐러드, 국밥 등\n카페") + } /// RestaurantMenuGroupCell - "영업 시간이 아니에요." - static let notBusinessHour: String = "영업 시간이 아니에요." + static var notBusinessHour: String { + Localization.localized("home.notBusinessHour", fallback: "영업 시간이 아니에요.") + } /// RestaurantTableViewHeader - "기숙사 식당" - static let dormitoryRestaurant: String = "기숙사 식당" + static var dormitoryRestaurant: String { + Localization.localized("home.dormitoryRestaurant", fallback: "기숙사 식당") + } } // MARK: - Map enum Map { /// MainMapVC - "제휴 지도" - static let map: String = "제휴 지도" + static var map: String { + Localization.localized("map.map", fallback: "제휴 지도") + } /// MainMapView - "전체" - static let all: String = "전체" + static var all: String { + Localization.localized("map.all", fallback: "전체") + } /// MainMapView - "내 제휴" - static let myPartner: String = "내 제휴" + static var myPartner: String { + Localization.localized("map.myPartner", fallback: "내 제휴") + } /// NoDepartmentSheetVC - "학과를 입력하고\n나만의 제휴를 확인해보세요!" - static let inputDepartment: String = "학과를 입력하고\n나만의 제휴를 확인해보세요!" + static var inputDepartment: String { + Localization.localized("map.inputDepartment", fallback: "학과를 입력하고\n나만의 제휴를 확인해보세요!") + } /// NoDepartmentSheetVC - "학과 입력하기" - static let inputDepartmentButton: String = "학과 입력하기" + static var inputDepartmentButton: String { + Localization.localized("map.inputDepartmentButton", fallback: "학과 입력하기") + } /// PartnershipDetailSheetVC - "음식점" - static let restaurant: String = "음식점" + static var restaurant: String { + Localization.localized("map.restaurant", fallback: "음식점") + } /// PartnershipDetailSheetVC - "카페" - static let cafe: String = "카페" + static var cafe: String { + Localization.localized("map.cafe", fallback: "카페") + } /// PartnershipDetailSheetVC - "주점" - static let pub: String = "주점" + static var pub: String { + Localization.localized("map.pub", fallback: "주점") + } /// PartnershipDetailSheetVC - "학과 정보 없음" - static let noDepartmentInfo: String = "학과 정보 없음" + static var noDepartmentInfo: String { + Localization.localized("map.noDepartmentInfo", fallback: "학과 정보 없음") + } /// MainMapVC+Location - "위치 권한 필요" - static let needLocationAuth: String = "위치 권한 필요" + static var needLocationAuth: String { + Localization.localized("map.needLocationAuth", fallback: "위치 권한 필요") + } /// MainMapVC+Location - "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요." - static let locationAuthDescription: String = "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요." + static var locationAuthDescription: String { + Localization.localized( + "map.locationAuthDescription", + fallback: "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요." + ) + } } // MARK: - MyPage enum MyPage { /// "마이페이지" - static let myPage: String = "마이페이지" - - /// "내 정보" - static let myInfo: String = "내 정보" + static var myPage: String { + Localization.localized("myPage.myPage", fallback: "마이페이지") + } - /// "내 리뷰" - static let myReview: String = "내 리뷰" - /// UserWithdrawVC - "회원탈퇴" - static let withdraw: String = "회원탈퇴" - - /// MyPageVC - "로그아웃" - static let logout: String = "로그아웃" - - /// MyPageVC - "정말 로그아웃 하시겠습니까?" - static let askLogout: String = "정말 로그아웃 하시겠습니까?" - + static var withdraw: String { + Localization.localized("myPage.withdraw", fallback: "회원탈퇴") + } + /// MyPageVC - "EAT-SSU 수신 동의" static func agreeNoti(date: String) -> String { - return "EAT-SSU 수신 동의 (\(date))" + return Localization.formatted("myPage.agreeNoti", fallback: "EAT-SSU 수신 동의 (%@)", date) } /// MyPageVC - "EAT-SSU 수신 거절" static func disagreeNoti(date: String) -> String { - return "EAT-SSU 수신 거절 (\(date))" + return Localization.formatted("myPage.disagreeNoti", fallback: "EAT-SSU 수신 거절 (%@)", date) + } + + // MARK: - MyPageSection: 알림 및 활동 + + /// MyPageSectionVC - "알림 및 활동" + static var activitySection: String { + Localization.localized("myPage.activitySection", fallback: "알림 및 활동") + } + + /// "내 정보" + static var myInfo: String { + Localization.localized("myPage.myInfo", fallback: "내 정보") + } + + /// "내 리뷰" + static var myReview: String { + Localization.localized("myPage.myReview", fallback: "내 리뷰") + } + + /// NotificationSettingTableViewCell - "푸시 알림 설정" + static var pushNotificationSetting: String { + Localization.localized("myPage.pushNotificationSetting", fallback: "푸시 알림 설정") } + + /// Push Notification key for UserDefaults + static let pushNotificationUserSettingKey: String = "pushNotificationUserSettingKey" + /// NotificationSettingTableViewCell - "매일 오전 11시에 알림을 보내드려요" + static var pushNotificationDescription: String { + Localization.localized("myPage.pushNotificationDescription", fallback: "매일 오전 11시에 알림을 보내드려요") + } + /// MyPageVC - "알림 설정 중 오류가 발생했습니다." - static let notiSettingError: String = "알림 설정 중 오류가 발생했습니다." + static var notiSettingError: String { + Localization.localized("myPage.notiSettingError", fallback: "알림 설정 중 오류가 발생했습니다.") + } + + // MARK: - MyPageSection: 서비스 정보 + + /// MyPageSectionVC - "서비스 정보" + static var serviceInfoSection: String { + Localization.localized("myPage.serviceInfoSection", fallback: "서비스 정보") + } + + /// MyPageVC - "문의하기" + static var inquiry: String { + Localization.localized("myPage.inquiry", fallback: "문의하기") + } /// CreatorVC - "만든 사람들" - static let creators: String = "만든 사람들" + static var creators: String { + Localization.localized("myPage.creators", fallback: "만든 사람들") + } + + /// MyPageVC - "EAT-SSU 인스타그램" + static var instagram: String { + Localization.localized("myPage.instagram", fallback: "EAT-SSU 인스타그램") + } + + // MARK: - MyPageSection: 기타 + + /// MyPageSectionVC - "기타" + static var etcSection: String { + Localization.localized("myPage.etcSection", fallback: "기타") + } + + /// MyPageVC - "언어 설정" + static var languageSetting: String { + Localization.localized("myPage.languageSetting", fallback: "언어 설정") + } + + /// MyPageVC - "현재 언어" + static var currentLanguage: String { + return AppLanguageManager.shared.currentLanguage.title + } + + /// MyPageVC - "약관 및 정책" + static var termsAndPolicy: String { + Localization.localized("myPage.termsAndPolicy", fallback: "약관 및 정책") + } + + /// MyPageVC - "서비스 이용약관" + static var termsOfUse: String { + Localization.localized("myPage.termsOfUse", fallback: "서비스 이용약관") + } + + /// MyPageVC - "개인정보처리방침" + static var privacyTermsOfUse: String { + Localization.localized("myPage.privacyTermsOfUse", fallback: "개인정보처리방침") + } + + /// MyPageVC - "로그아웃" + static var logout: String { + Localization.localized("myPage.logout", fallback: "로그아웃") + } + + /// MyPageVC - "정말 로그아웃 하시겠습니까?" + static var askLogout: String { + Localization.localized("myPage.askLogout", fallback: "정말 로그아웃 하시겠습니까?") + } /// MyReviewVC - "리뷰 수정 혹은 삭제" - static let fixOrDeleteReview: String = "리뷰 수정 혹은 삭제" + static var fixOrDeleteReview: String { + Localization.localized("myPage.fixOrDeleteReview", fallback: "리뷰 수정 혹은 삭제") + } /// MyReviewVC - "작성하신 리뷰를 수정 또는 삭제하시겠습니까?" - static let askFixOrDeleteReview: String = "작성하신 리뷰를 수정 또는 삭제하시겠습니까?" + static var askFixOrDeleteReview: String { + Localization.localized("myPage.askFixOrDeleteReview", fallback: "작성하신 리뷰를 수정 또는 삭제하시겠습니까?") + } /// MyReviewVC - "리뷰 삭제하기" - static let deleteMyReview: String = "리뷰 삭제하기" + static var deleteMyReview: String { + Localization.localized("myPage.deleteMyReview", fallback: "리뷰 삭제하기") + } /// MyReviewVC - "해당 리뷰를 삭제할까요?" - static let askDeleteMyReview: String = "해당 리뷰를 삭제할까요?" + static var askDeleteMyReview: String { + Localization.localized("myPage.askDeleteMyReview", fallback: "해당 리뷰를 삭제할까요?") + } /// MyReviewVC - "리뷰가 성공적으로 삭제되었습니다." - static let deleteMyReviewSuccess: String = "리뷰가 성공적으로 삭제되었습니다." + static var deleteMyReviewSuccess: String { + Localization.localized("myPage.deleteMyReviewSuccess", fallback: "리뷰가 성공적으로 삭제되었습니다.") + } /// MyPageView - "다시 시도해주세요" - static let retry: String = "다시 시도해주세요" + static var retry: String { + Localization.localized("myPage.retry", fallback: "다시 시도해주세요") + } /// MyPageView - "앱 버전" - static let appVersion: String = "앱 버전" + static var appVersion: String { + Localization.localized("myPage.appVersion", fallback: "앱 버전") + } /// MyPageView - "탈퇴하기" - static let withdrawButton: String = "탈퇴하기" + static var withdrawButton: String { + Localization.localized("myPage.withdrawButton", fallback: "탈퇴하기") + } /// MyPageView - "알 수 없음" - static let unknownUser: String = "알 수 없음" - - /// NotificationSettingTableViewCell - "푸시 알림 설정" - static let pushNotificationSetting: String = "푸시 알림 설정" - - /// Push Notification key for UserDefaults - static let pushNotificationUserSettingKey: String = "pushNotificationUserSettingKey" - - /// NotificationSettingTableViewCell - "매일 오전 11시에 알림을 보내드려요" - static let pushNotificationDescription: String = "매일 오전 11시에 알림을 보내드려요" - - /// MyPageVC - "문의하기" - static let inquiry: String = "문의하기" - - /// MyPageVC - "서비스 이용약관" - static let termsOfUse: String = "서비스 이용약관" - - /// MyPageVC - "개인정보 이용약관" - static let privacyTermsOfUse: String = "개인정보 이용약관" + static var unknownUser: String { + Localization.localized("myPage.unknownUser", fallback: "알 수 없음") + } /// ProvisionVC - "이용약관" - static let defaultTerms: String = "이용약관" - + static var defaultTerms: String { + Localization.localized("myPage.defaultTerms", fallback: "이용약관") + } + /// UserWithdrawView - "정말 탈퇴하시겠습니까?" - static let confirmWithdrawal: String = "정말 탈퇴하시겠습니까?" + static var confirmWithdrawal: String { + Localization.localized("myPage.confirmWithdrawal", fallback: "정말 탈퇴하시겠습니까?") + } /// UserWithdrawView - "작성한 리뷰 게시글은 삭제되지 않으며, (알수없음)으로 표시됩니다.\n자세한 내용은 서비스이용약관 및 개인정보처리방침을 확인해 주세요." - static let withdrawalNotice: String = "작성한 리뷰 게시글은 삭제되지 않으며, (알수없음)으로 표시됩니다.\n자세한 내용은 서비스이용약관 및 개인정보처리방침을 확인해 주세요." + static var withdrawalNotice: String { + Localization.localized( + "myPage.withdrawalNotice", + fallback: "작성한 리뷰 게시글은 삭제되지 않으며, (알수없음)으로 표시됩니다.\n자세한 내용은 서비스이용약관 및 개인정보처리방침을 확인해 주세요." + ) + } /// UserWithdrawView - "올바른 입력입니다." - static let validInputMessage: String = "올바른 입력입니다" + static var validInputMessage: String { + Localization.localized("myPage.validInputMessage", fallback: "올바른 입력입니다") + } /// UserWithdrawView - "올바르지 않은 닉네임입니다" - static let invalidNicknameMessage: String = "올바르지 않은 닉네임입니다" + static var invalidNicknameMessage: String { + Localization.localized("myPage.invalidNicknameMessage", fallback: "올바르지 않은 닉네임입니다") + } } // MARK: - Review enum Review { /// ReportVC - "EAT SSU 팀에게 보내기" - static let sendToTeam: String = "EAT SSU 팀에게 보내기" + static var sendToTeam: String { + Localization.localized("review.sendToTeam", fallback: "EAT SSU 팀에게 보내기") + } /// ReportVC - "신고하기" - static let report: String = "신고하기" + static var report: String { + Localization.localized("review.report", fallback: "신고하기") + } /// ReportVC - "사유를 선택해주세요!" - static let selectReason: String = "사유를 선택해주세요!" + static var selectReason: String { + Localization.localized("review.selectReason", fallback: "사유를 선택해주세요!") + } /// ReportVC - "신고가 성공적으로 접수되었어요!" - static let reportSuccess: String = "신고가 성공적으로 접수되었어요!" + static var reportSuccess: String { + Localization.localized("review.reportSuccess", fallback: "신고가 성공적으로 접수되었어요!") + } /// ReportVC, ReportView - "메뉴와 관련없는 내용" - static let unrelatedMenu: String = "메뉴와 관련없는 내용" + static var unrelatedMenu: String { + Localization.localized("review.unrelatedMenu", fallback: "메뉴와 관련없는 내용") + } /// ReportVC, ReportView - "음란성, 욕설 등 부적절한 내용" - static let inappropriateContent: String = "음란성, 욕설 등 부적절한 내용" + static var inappropriateContent: String { + Localization.localized("review.inappropriateContent", fallback: "음란성, 욕설 등 부적절한 내용") + } /// ReportVC, ReportView - "부적절한 홍보 또는 광고" - static let inappropriateAd: String = "부적절한 홍보 또는 광고" + static var inappropriateAd: String { + Localization.localized("review.inappropriateAd", fallback: "부적절한 홍보 또는 광고") + } /// ReportVC, ReportView - "리뷰 작성 취지에 맞지 않는 내용 (복사글 등)" - static let notReviewFormat: String = "리뷰 작성 취지에 맞지 않는 내용 (복사글 등)" + static var notReviewFormat: String { + Localization.localized("review.notReviewFormat", fallback: "리뷰 작성 취지에 맞지 않는 내용 (복사글 등)") + } /// ReportVC, ReportView - "저작권 도용 의심 (사진 등)" - static let copyright: String = "저작권 도용 의심 (사진 등)" + static var copyright: String { + Localization.localized("review.copyright", fallback: "저작권 도용 의심 (사진 등)") + } /// ReportVC, ReportView - "기타 (하단 내용 작성)" - static let etc: String = "기타 (하단 내용 작성)" + static var etc: String { + Localization.localized("review.etc", fallback: "기타 (하단 내용 작성)") + } /// ReportView - "리뷰 신고 사유를 알려주세요" - static let reportReason: String = "리뷰 신고 사유를 알려주세요" + static var reportReason: String { + Localization.localized("review.reportReason", fallback: "리뷰 신고 사유를 알려주세요") + } /// ReportView - "하나의 리뷰에 대해 24시간 내 한 번만 신고 가능합니다." - static let reportGuide: String = "하나의 리뷰에 대해 24시간 내 한 번만 신고 가능합니다." + static var reportGuide: String { + Localization.localized("review.reportGuide", fallback: "하나의 리뷰에 대해 24시간 내 한 번만 신고 가능합니다.") + } /// ReportView - "리뷰 신고 사유를 작성해 주세요" - static let inputReportReason: String = "리뷰 신고 사유를 작성해 주세요" + static var inputReportReason: String { + Localization.localized("review.inputReportReason", fallback: "리뷰 신고 사유를 작성해 주세요") + } /// ReviewVC - "리뷰 작성하기" - static let writeReview: String = "리뷰 작성하기" + static var writeReview: String { + Localization.localized("review.writeReview", fallback: "리뷰 작성하기") + } /// ReviewVC - "리뷰가 성공적으로 등록되었습니다." - static let registerReviewSuccess: String = "리뷰가 성공적으로 등록되었습니다." + static var registerReviewSuccess: String { + Localization.localized("review.registerReviewSuccess", fallback: "리뷰가 성공적으로 등록되었습니다.") + } /// ReviewVC - "리뷰" - static let review: String = "리뷰" + static var review: String { + Localization.localized("review.review", fallback: "리뷰") + } /// ReviewVC - "리뷰 삭제" - static let deleteReview: String = "리뷰 삭제" + static var deleteReview: String { + Localization.localized("review.deleteReview", fallback: "리뷰 삭제") + } /// ReviewVC - "해당 리뷰를 삭제할까요?" - static let askDeleteReview: String = "해당 리뷰를 삭제할까요?" + static var askDeleteReview: String { + Localization.localized("review.askDeleteReview", fallback: "해당 리뷰를 삭제할까요?") + } /// ReviewVC - "리뷰 신고하기" - static let reportReview: String = "리뷰 신고하기" + static var reportReview: String { + Localization.localized("review.reportReview", fallback: "리뷰 신고하기") + } /// ReviewVC - "해당 리뷰를 신고하시겠습니까?" - static let askReportReview: String = "해당 리뷰를 신고하시겠습니까?" + static var askReportReview: String { + Localization.localized("review.askReportReview", fallback: "해당 리뷰를 신고하시겠습니까?") + } /// ReviewVC - "리뷰가 성공적으로 삭제되었습니다." - static let deleteReviewSuccess: String = "리뷰가 성공적으로 삭제되었습니다." + static var deleteReviewSuccess: String { + Localization.localized("review.deleteReviewSuccess", fallback: "리뷰가 성공적으로 삭제되었습니다.") + } /// ReviewVC - "리뷰 삭제에 실패했습니다." - static let deleteReviewFail: String = "리뷰 삭제에 실패했습니다." + static var deleteReviewFail: String { + Localization.localized("review.deleteReviewFail", fallback: "리뷰 삭제에 실패했습니다.") + } /// SetRateVC - "리뷰 수정하기" - static let fixReview: String = "리뷰 수정하기" + static var fixReview: String { + Localization.localized("review.fixReview", fallback: "리뷰 수정하기") + } /// SetRateVC - "리뷰 남기기" - static let leaveReview: String = "리뷰 남기기" + static var leaveReview: String { + Localization.localized("review.leaveReview", fallback: "리뷰 남기기") + } /// 메뉴 이름의 받침 유무에 따라 '을/를'을 동적으로 붙여 추천 문장을 생성합니다. static func recommendMenu(name: String) -> String { guard let lastChar = name.last, let lastScalar = lastChar.unicodeScalars.first else { - return "\(name)을(를) 추천하시겠어요?" // 예외 처리 + return Localization.formatted("review.recommendMenu.default", fallback: "%@을(를) 추천하시겠어요?", name) } - // '가' ~ '힣' 사이의 한글 유니코드 범위 let hangulStart: UInt32 = 0xAC00 let hangulEnd: UInt32 = 0xD7A3 - // 받침이 있는지 계산 (종성 코드 확인) if lastScalar.value >= hangulStart && lastScalar.value <= hangulEnd { let hasJongseong = (lastScalar.value - hangulStart) % 28 != 0 if hasJongseong { - return "\(name)을 추천하시겠어요?" // 받침 있음 + return Localization.formatted("review.recommendMenu.withJongseong", fallback: "%@을 추천하시겠어요?", name) } } - return "\(name)를 추천하시겠어요?" // 받침 없음 + return Localization.formatted("review.recommendMenu.withoutJongseong", fallback: "%@를 추천하시겠어요?", name) } /// SetRateVC - "메뉴를 추천하시겠어요?" - static let recommendMenuTitle: String = "메뉴를 추천하시겠어요?" + static var recommendMenuTitle: String { + Localization.localized("review.recommendMenuTitle", fallback: "메뉴를 추천하시겠어요?") + } /// SetRateVC - "리뷰 수정 완료하기" - static let fixReviewComplete: String = "리뷰 수정 완료하기" + static var fixReviewComplete: String { + Localization.localized("review.fixReviewComplete", fallback: "리뷰 수정 완료하기") + } /// SetRateVC - "완료하기" - static let complete: String = "완료하기" + static var complete: String { + Localization.localized("review.complete", fallback: "완료하기") + } /// SetRateVC - "별점을 입력해주세요!" - static let inputRating: String = "별점을 입력해주세요!" + static var inputRating: String { + Localization.localized("review.inputRating", fallback: "별점을 입력해주세요!") + } /// SetRateVC - "메뉴 목록 조회에 실패했습니다." - static let loadMenuListFail: String = "메뉴 목록 조회에 실패했습니다." + static var loadMenuListFail: String { + Localization.localized("review.loadMenuListFail", fallback: "메뉴 목록 조회에 실패했습니다.") + } /// SetRateVC - "수정할 리뷰 정보가 없습니다." - static let noReviewInfoForFix: String = "수정할 리뷰 정보가 없습니다." + static var noReviewInfoForFix: String { + Localization.localized("review.noReviewInfoForFix", fallback: "수정할 리뷰 정보가 없습니다.") + } /// SetRateVC - "리뷰가 성공적으로 수정되었습니다." - static let fixReviewSuccess: String = "리뷰가 성공적으로 수정되었습니다." + static var fixReviewSuccess: String { + Localization.localized("review.fixReviewSuccess", fallback: "리뷰가 성공적으로 수정되었습니다.") + } /// SetRateVC - "리뷰 수정에 실패했습니다." - static let fixReviewFail: String = "리뷰 수정에 실패했습니다." + static var fixReviewFail: String { + Localization.localized("review.fixReviewFail", fallback: "리뷰 수정에 실패했습니다.") + } /// SetRateVC - "식단 정보가 없습니다." - static let noMealInfo: String = "식단 정보가 없습니다." + static var noMealInfo: String { + Localization.localized("review.noMealInfo", fallback: "식단 정보가 없습니다.") + } /// SetRateVC - "리뷰 업로드에 실패했습니다." - static let uploadReviewFail: String = "리뷰 업로드에 실패했습니다." + static var uploadReviewFail: String { + Localization.localized("review.uploadReviewFail", fallback: "리뷰 업로드에 실패했습니다.") + } /// SetRateVC - "메뉴 정보가 없습니다." - static let noMenuInfo: String = "메뉴 정보가 없습니다." + static var noMenuInfo: String { + Localization.localized("review.noMenuInfo", fallback: "메뉴 정보가 없습니다.") + } /// SetRateVC - "메뉴에 대한 상세한 리뷰를 작성해주세요" - static let inputDetailReview: String = "메뉴에 대한 상세한 리뷰를 작성해주세요" + static var inputDetailReview: String { + Localization.localized("review.inputDetailReview", fallback: "메뉴에 대한 상세한 리뷰를 작성해주세요") + } /// SetRateVC - "나가시겠어요?" - static let askLeave: String = "나가시겠어요?" + static var askLeave: String { + Localization.localized("review.askLeave", fallback: "나가시겠어요?") + } /// SetRateVC - "지금 나가면 작성한 내용이 저장되지 않습니다." - static let leaveWarning: String = "지금 나가면 작성한 내용이 저장되지 않습니다." + static var leaveWarning: String { + Localization.localized("review.leaveWarning", fallback: "지금 나가면 작성한 내용이 저장되지 않습니다.") + } /// SetRateVC - "나가기" - static let leave: String = "나가기" + static var leave: String { + Localization.localized("review.leave", fallback: "나가기") + } /// SetRateVC - "계속 작성" - static let continueWriting: String = "계속 작성" + static var continueWriting: String { + Localization.localized("review.continueWriting", fallback: "계속 작성") + } /// ReviewEmptyViewCell - "아직 작성된 리뷰가 없어요!" - static let noReview: String = "아직 작성된 리뷰가 없어요!" + static var noReview: String { + Localization.localized("review.noReview", fallback: "아직 작성된 리뷰가 없어요!") + } /// ReviewEmptyViewCell - "메뉴에 가장 먼저 리뷰를 남겨주세요!" - static let beFirstReviewer: String = "메뉴에 가장 먼저 리뷰를 남겨주세요!" + static var beFirstReviewer: String { + Localization.localized("review.beFirstReviewer", fallback: "메뉴에 가장 먼저 리뷰를 남겨주세요!") + } /// ReviewEmptyViewCell - "로그인이 필요합니다" - static let needLogin: String = "로그인이 필요합니다" + static var needLogin: String { + Localization.localized("review.needLogin", fallback: "로그인이 필요합니다") + } /// ReviewEmptyViewCell - "로그인 후 리뷰를 확인하세요" - static let checkReviewAfterLogin: String = "로그인 후 리뷰를 확인하세요" + static var checkReviewAfterLogin: String { + Localization.localized("review.checkReviewAfterLogin", fallback: "로그인 후 리뷰를 확인하세요") + } /// ReviewEmptyViewCell - "아직 작성한 리뷰가 없어요" - static let noWrittenReview: String = "아직 작성한 리뷰가 없어요" + static var noWrittenReview: String { + Localization.localized("review.noWrittenReview", fallback: "아직 작성한 리뷰가 없어요") + } /// ReviewEmptyViewCell - "첫 리뷰를 남겨 주세요!" - static let writeFirstReview: String = "첫 리뷰를 남겨 주세요!" + static var writeFirstReview: String { + Localization.localized("review.writeFirstReview", fallback: "첫 리뷰를 남겨 주세요!") + } /// ReviewDividerCell - "리뷰" static func reviewCount(_ count: Int) -> String { - return "리뷰 \(count)" + return Localization.formatted("review.reviewCount", fallback: "리뷰 %d", count) } /// ReviewRateViewCell - "오늘의 메뉴" - static let todayMenu: String = "오늘의 메뉴" + static var todayMenu: String { + Localization.localized("review.todayMenu", fallback: "오늘의 메뉴") + } /// ReviewRateViewCell - "5점" - static let fiveStars: String = "5점" + static var fiveStars: String { + Localization.localized("review.fiveStars", fallback: "5점") + } /// ReviewRateViewCell - "4점" - static let fourStars: String = "4점" + static var fourStars: String { + Localization.localized("review.fourStars", fallback: "4점") + } /// ReviewRateViewCell - "3점" - static let threeStars: String = "3점" + static var threeStars: String { + Localization.localized("review.threeStars", fallback: "3점") + } /// ReviewRateViewCell - "2점" - static let twoStars: String = "2점" + static var twoStars: String { + Localization.localized("review.twoStars", fallback: "2점") + } /// ReviewRateViewCell - "1점" - static let oneStar: String = "1점" + static var oneStar: String { + Localization.localized("review.oneStar", fallback: "1점") + } /// SetRateView - "오늘의 식사는 어떠셨나요?" - static let rateTodayMeal: String = "오늘의 식사는 어떠셨나요?" + static var rateTodayMeal: String { + Localization.localized("review.rateTodayMeal", fallback: "오늘의 식사는 어떠셨나요?") + } /// SetRateView - "추천하고 싶은 메뉴가 있나요?" - static let recommendMenu: String = "추천하고 싶은 메뉴가 있나요?" + static var recommendMenu: String { + Localization.localized("review.recommendMenu", fallback: "추천하고 싶은 메뉴가 있나요?") + } /// SetRateView - "사진 추가 (0/1)" static func addPhoto(count: Int) -> String { - return "사진 추가 (\(count)/1)" + return Localization.formatted("review.addPhoto", fallback: "사진 추가 (%d/1)", count) } /// character count static func characterCount(current: Int, max: Int) -> String { - return "\(current) / \(max)" + return Localization.formatted("review.characterCount", fallback: "%d / %d", current, max) } } @@ -589,66 +993,132 @@ enum TextLiteral { enum Coffee { /// "나가시겠어요?" - static let askLeave: String = "나가시겠어요?" + static var askLeave: String { + Localization.localized("coffee.askLeave", fallback: "나가시겠어요?") + } /// "지금 나가면 진행 상황이\n저장되지 않습니다." - static let leaveWarning: String = "지금 나가면 진행 상황이\n저장되지 않습니다." + static var leaveWarning: String { + Localization.localized("coffee.leaveWarning", fallback: "지금 나가면 진행 상황이\n저장되지 않습니다.") + } /// "나가기" - static let leave: String = "나가기" + static var leave: String { + Localization.localized("coffee.leave", fallback: "나가기") + } /// "계속하기" - static let continueEvent: String = "계속하기" + static var continueEvent: String { + Localization.localized("coffee.continueEvent", fallback: "계속하기") + } } // MARK: - Splash enum Splash { /// NoticeSplashVC - "긴급 서버 점검 안내" - static let serverInspection: String = "긴급 서버 점검 안내" + static var serverInspection: String { + Localization.localized("splash.serverInspection", fallback: "긴급 서버 점검 안내") + } } // MARK: - PromotionPopup enum PromotionPopup { /// 03. 16(월)~03. 27(금) - static let period: String = "03. 16(월)~03. 27(금)" + static var period: String { + Localization.localized("promotionPopup.period", fallback: "03. 16(월)~03. 27(금)") + } /// EAT-SSU 인스타그램 바로가기 - static let instagramButtonTitle: String = "EAT-SSU 인스타그램 바로가기" + static var instagramButtonTitle: String { + Localization.localized("promotionPopup.instagramButtonTitle", fallback: "EAT-SSU 인스타그램 바로가기") + } /// 자세한 내용은 EAT-SSU 인스타그램을 확인해 주세요 - static let guideMessage: String = "자세한 내용은 EAT-SSU 인스타그램을 확인해 주세요" + static var guideMessage: String { + Localization.localized("promotionPopup.guideMessage", fallback: "자세한 내용은 EAT-SSU 인스타그램을 확인해 주세요") + } /// 다시 보지 않기 - static let neverShowAgain: String = "다시 보지 않기" + static var neverShowAgain: String { + Localization.localized("promotionPopup.neverShowAgain", fallback: "다시 보지 않기") + } /// 닫기 - static let close: String = "닫기" + static var close: String { + Localization.localized("promotionPopup.close", fallback: "닫기") + } } // MARK: - Notification enum Notification { /// 🤔 오늘 밥 뭐 먹지… - static let dailyWeekdayNotificationTitle: String = "🤔 오늘 밥 뭐 먹지…" + static var dailyWeekdayNotificationTitle: String { + Localization.localized("notification.dailyWeekdayNotificationTitle", fallback: "🤔 오늘 밥 뭐 먹지…") + } /// 오늘의 학식을 확인해보세요! - static let dailyWeekdayNotificationBody: String = "오늘의 학식을 확인해보세요!" + static var dailyWeekdayNotificationBody: String { + Localization.localized("notification.dailyWeekdayNotificationBody", fallback: "오늘의 학식을 확인해보세요!") + } + /// 알림 권한 필요 + static var permissionDeniedMessage: String { + Localization.localized( + "notification_error_permission_denied_message", + fallback: "알림 권한 필요" + ) + } + /// 알림을 받으려면 설정에서 알림 권한을 허용해주세요. + static var permissionDeniedDescription: String { + Localization.localized( + "notification_error_permission_denied_description", + fallback: "알림을 받으려면 설정에서 알림 권한을 허용해주세요." + ) + } + /// 알 수 없는 오류 + static var unknownErrorMessage: String { + Localization.localized( + "notification_error_unknown_message", + fallback: "알 수 없는 오류" + ) + } + /// 다시 시도해주세요. + static var unknownErrorDescription: String { + Localization.localized( + "notification_error_unknown_description", + fallback: "다시 시도해주세요." + ) + } } // MARK: - Restaurant enum Restaurant { /// "기숙사 식당" - static let dormitoryRestaurant: String = "기숙사 식당" + static var dormitoryRestaurant: String { + Localization.localized("restaurant.dormitoryRestaurant", fallback: "기숙사 식당") + } + /// "도담 식당" - static let dodamRestaurant: String = "도담 식당" + static var dodamRestaurant: String { + Localization.localized("restaurant.dodamRestaurant", fallback: "도담 식당") + } + /// "학생 식당" - static let studentRestaurant: String = "학생 식당" + static var studentRestaurant: String { + Localization.localized("restaurant.studentRestaurant", fallback: "학생 식당") + } + /// "스낵 코너" - static let snackCorner: String = "스낵 코너" + static var snackCorner: String { + Localization.localized("restaurant.snackCorner", fallback: "스낵 코너") + } + /// "FACULTY (교직원 전용)" - static let facultyRestaurant: String = "FACULTY (교직원 전용)" + static var facultyRestaurant: String { + Localization.localized("restaurant.facultyRestaurant", fallback: "FACULTY (교직원 전용)") + } } } diff --git a/EATSSU/App/Sources/Utility/UIComponent/CustomRadioButton.swift b/EATSSU/App/Sources/Utility/UIComponent/CustomRadioButton.swift new file mode 100644 index 00000000..3c7efec1 --- /dev/null +++ b/EATSSU/App/Sources/Utility/UIComponent/CustomRadioButton.swift @@ -0,0 +1,53 @@ +// +// CustomRadioButton.swift +// EATSSU +// +// Created by jeongminji on 5/3/26. +// + +import UIKit + +import EATSSUDesign + +final class CustomRadioButton: UIButton { + + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + + configureUI() + updateState(isSelected: false) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + layer.cornerRadius = bounds.width / 2 + } + + // MARK: - Function + + private func configureUI() { + backgroundColor = .clear + layer.borderWidth = 2 + layer.borderColor = UIColor.gray400.cgColor + } + + func updateState(isSelected: Bool) { + self.isSelected = isSelected + + layer.borderWidth = isSelected ? 6 : 2 + layer.borderColor = isSelected + ? UIColor.primary.cgColor + : UIColor.gray400.cgColor + + backgroundColor = .clear + setNeedsLayout() + } +} diff --git a/EATSSU/App/Sources/Utility/UIComponent/SocialLoginButton.swift b/EATSSU/App/Sources/Utility/UIComponent/SocialLoginButton.swift new file mode 100644 index 00000000..49dcc845 --- /dev/null +++ b/EATSSU/App/Sources/Utility/UIComponent/SocialLoginButton.swift @@ -0,0 +1,154 @@ +// +// SocialLoginButton.swift +// EATSSU +// +// Created by jeongminji on 5/4/26. +// + +import UIKit + +import SnapKit + +import EATSSUDesign + +// MARK: - extension UIColor: 카카오 로그인 버튼 규격 + +private extension UIColor { + static let kakaoContainer = UIColor( + red: 254 / 255, + green: 229 / 255, + blue: 0 / 255, + alpha: 1.0 + ) + + static let kakaoSymbol = UIColor.black + + static let kakaoLabel = UIColor.black.withAlphaComponent(0.85) +} + +final class SocialLoginButton: UIButton { + + // MARK: - Properties + + enum LoginType { + case apple + case kakao + + var title: String { + switch self { + case .apple: + return TextLiteral.Auth.signInWithApple + case .kakao: + return TextLiteral.Auth.signInWithKakao + } + } + + var backgroundColor: UIColor { + switch self { + case .apple: + return .black + case .kakao: + return .kakaoContainer + } + } + + var titleColor: UIColor { + switch self { + case .apple: + return .white + case .kakao: + return .kakaoLabel + } + } + + var icon: UIImage { + switch self { + case .apple: + return EATSSUDesignAsset.Images.appleLoginLogo.image + case .kakao: + return EATSSUDesignAsset.Images.kakaoLoginLogo.image + } + } + + var iconVerticalInset: CGFloat { + switch self { + case .apple: + return 11 + case .kakao: + return 14 + } + } + + var iconAspectRatio: CGFloat { + return icon.size.width / icon.size.height + } + } + + private enum Constant { + static let buttonAspectRatio = 45.0 / 300.0 + } + + private let type: LoginType + + // MARK: - UI Components + + private let iconImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + return imageView + }() + + private let loginTitleLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 15, weight: .regular) + label.textAlignment = .center + return label + }() + + // MARK: - Initializer + + init(type: LoginType) { + self.type = type + super.init(frame: .zero) + + configureUI() + setLayout() + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Functions + + private func configureUI() { + layer.cornerRadius = 5 + clipsToBounds = true + + addSubviews(iconImageView, loginTitleLabel) + } + + private func setLayout() { + snp.makeConstraints { + $0.height.equalTo(snp.width).multipliedBy(Constant.buttonAspectRatio) + } + + iconImageView.snp.makeConstraints { + $0.leading.equalToSuperview().inset(15) + $0.verticalEdges.equalToSuperview().inset(type.iconVerticalInset) + $0.width.equalTo(iconImageView.snp.height).multipliedBy(type.iconAspectRatio) + } + + loginTitleLabel.snp.makeConstraints { + $0.center.equalToSuperview() + } + } + + private func configure() { + backgroundColor = type.backgroundColor + iconImageView.image = type.icon + loginTitleLabel.text = type.title + loginTitleLabel.textColor = type.titleColor + } +} diff --git a/EATSSU/Project.swift b/EATSSU/Project.swift index b86afd34..a181d045 100644 --- a/EATSSU/Project.swift +++ b/EATSSU/Project.swift @@ -99,7 +99,7 @@ let widgetDeploymentTarget: DeploymentTargets = .iOS("17.0") let project = Project( name: "EATSSU", options: .options( - defaultKnownRegions: ["ko"], + defaultKnownRegions: ["ko", "en"], developmentRegion: "ko" ), settings: projectSettings, diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/AppleLoginButton.imageset/AppleLoginButton.svg b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/AppleLoginButton.imageset/AppleLoginButton.svg deleted file mode 100644 index 2177a536..00000000 --- a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/AppleLoginButton.imageset/AppleLoginButton.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/AppleLoginButton.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/AppleLoginButton.imageset/Contents.json deleted file mode 100644 index f1a1f5b2..00000000 --- a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/AppleLoginButton.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "AppleLoginButton.svg", - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/AppleLoginLogo.imageset/AppleLoginLogo.svg b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/AppleLoginLogo.imageset/AppleLoginLogo.svg new file mode 100644 index 00000000..31f0adeb --- /dev/null +++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/AppleLoginLogo.imageset/AppleLoginLogo.svg @@ -0,0 +1,3 @@ + + + diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoLoginButton.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/AppleLoginLogo.imageset/Contents.json similarity index 74% rename from EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoLoginButton.imageset/Contents.json rename to EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/AppleLoginLogo.imageset/Contents.json index 1cb770d4..7d70c8a6 100644 --- a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoLoginButton.imageset/Contents.json +++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/AppleLoginLogo.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "KakaoLoginButton.svg", + "filename" : "AppleLoginLogo.svg", "idiom" : "universal" } ], diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoLoginButton.imageset/KakaoLoginButton.svg b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoLoginButton.imageset/KakaoLoginButton.svg deleted file mode 100644 index d6710f9b..00000000 --- a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoLoginButton.imageset/KakaoLoginButton.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/LookAroundButton.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoLoginLogo.imageset/Contents.json similarity index 74% rename from EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/LookAroundButton.imageset/Contents.json rename to EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoLoginLogo.imageset/Contents.json index 0e0074c0..4823ae8d 100644 --- a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/LookAroundButton.imageset/Contents.json +++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoLoginLogo.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "LookAroundButton.svg", + "filename" : "KakaoLoginLogo.svg", "idiom" : "universal" } ], diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoLoginLogo.imageset/KakaoLoginLogo.svg b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoLoginLogo.imageset/KakaoLoginLogo.svg new file mode 100644 index 00000000..636993a2 --- /dev/null +++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoLoginLogo.imageset/KakaoLoginLogo.svg @@ -0,0 +1,3 @@ + + + diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/LookAroundButton.imageset/LookAroundButton.svg b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/LookAroundButton.imageset/LookAroundButton.svg deleted file mode 100644 index 2824bdd5..00000000 --- a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/LookAroundButton.imageset/LookAroundButton.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -