diff --git a/apps/mobile/src/features/club/screens/ClubInfoEditScreen/index.tsx b/apps/mobile/src/features/club/screens/ClubInfoEditScreen/index.tsx index c791382c..808b487a 100644 --- a/apps/mobile/src/features/club/screens/ClubInfoEditScreen/index.tsx +++ b/apps/mobile/src/features/club/screens/ClubInfoEditScreen/index.tsx @@ -260,14 +260,15 @@ SuccessModal.displayName = "SuccessModal"; type ClubCategorySectionProps = { category: ClubInfoEditFormData["category"]; + isMissing: boolean; onChange: (category: ClubInfoEditFormData["category"]) => void; }; const ClubCategorySection = memo( - ({ category, onChange }: ClubCategorySectionProps) => { + ({ category, isMissing, onChange }: ClubCategorySectionProps) => { return ( - 카테고리 + *카테고리 {CLUB_CATEGORIES.map((categoryOption) => { const isSelected = category === categoryOption; @@ -292,6 +293,9 @@ const ClubCategorySection = memo( ); })} + {isMissing && ( + 카테고리는 필수 입력 정보예요 + )} ); }, @@ -301,14 +305,15 @@ ClubCategorySection.displayName = "ClubCategorySection"; type ClubRecruitTypeSectionProps = { recruitType: string; + isMissing: boolean; onChange: (recruitType: string) => void; }; const ClubRecruitTypeSection = memo( - ({ recruitType, onChange }: ClubRecruitTypeSectionProps) => { + ({ recruitType, isMissing, onChange }: ClubRecruitTypeSectionProps) => { return ( - 모집 형태 + *모집 형태 {CLUB_RECRUIT_TYPES.map((recruitTypeOption) => { const isSelected = recruitType === recruitTypeOption; @@ -333,6 +338,11 @@ const ClubRecruitTypeSection = memo( ); })} + {isMissing && ( + + 모집 형태는 필수 입력 정보예요 + + )} ); }, @@ -497,6 +507,11 @@ type ClubBasicInfoSectionProps = { department: string; shortIntro: string; showDepartmentDropdown: boolean; + isImageMissing: boolean; + isNameMissing: boolean; + isBasicInfoMissing: boolean; + isDepartmentMissing: boolean; + isShortIntroMissing: boolean; onSelectImage: () => void; onClubNameChange: (value: string) => void; onToggleDepartmentDropdown: () => void; @@ -510,6 +525,11 @@ const ClubBasicInfoSection = memo( department, shortIntro, showDepartmentDropdown, + isImageMissing, + isNameMissing, + isBasicInfoMissing, + isDepartmentMissing, + isShortIntroMissing, onSelectImage, onClubNameChange, onToggleDepartmentDropdown, @@ -517,11 +537,17 @@ const ClubBasicInfoSection = memo( }: ClubBasicInfoSectionProps) => ( <> - 동아리 대표 이미지 + *동아리 대표 이미지 4X4 정방형 이미지를 권장드려요 - + {clubImage ? ( ) : ( @@ -531,25 +557,40 @@ const ClubBasicInfoSection = memo( 이미지 변경 + {isImageMissing && ( + + 대표 이미지는 필수 입력 정보예요 + + )} - 동아리명 + *동아리명 + {isNameMissing && ( + 동아리명은 필수 입력 정보예요 + )} - 한줄소개 + *한줄소개 @@ -566,11 +607,23 @@ const ClubBasicInfoSection = memo( + {isBasicInfoMissing && ( + + 동아리 한줄소개는 필수 입력 정보예요 + + )} ), @@ -580,23 +633,28 @@ ClubBasicInfoSection.displayName = "ClubBasicInfoSection"; type ClubDescriptionSectionProps = { description: string; + isMissing: boolean; onChange: (value: string) => void; }; const ClubDescriptionSection = memo( - ({ description, onChange }: ClubDescriptionSectionProps) => ( + ({ description, isMissing, onChange }: ClubDescriptionSectionProps) => ( - 동아리 추가 설명 - *동아리 추가 설명 + + {isMissing && ( + + 동아리 추가 설명은 필수 입력 정보예요 + + )} ), ); @@ -912,15 +970,24 @@ const ClubInfoEditScreen = () => { JSON.stringify(initialComparable?.snsUrls) || areValidSnsUrls(formData.clubSNSUrls); + const isImageMissing = !formData.clubImage; + const isNameMissing = !formData.clubName.trim(); + const isDepartmentMissing = !formData.department.trim(); + const isShortIntroMissing = !formData.shortIntro.trim(); + const isBasicInfoMissing = isDepartmentMissing || isShortIntroMissing; + const isCategoryMissing = !formData.category; + const isRecruitTypeMissing = !formData.recruitType.trim(); + const isSnsMissing = !isSNSComplete; + const isDescriptionMissing = !formData.clubDescription.trim(); + const isComplete = - !!formData.clubImage && - !!formData.clubName.trim() && - !!formData.category && - !!formData.department.trim() && - !!formData.shortIntro.trim() && - !!formData.recruitType.trim() && + !isImageMissing && + !isNameMissing && + !isCategoryMissing && + !isBasicInfoMissing && + !isRecruitTypeMissing && isSNSComplete && - !!formData.clubDescription.trim() && + !isDescriptionMissing && (minActivityPeriodMode === "none" || !!formData.minActivityPeriodInput.trim()); @@ -1061,6 +1128,11 @@ const ClubInfoEditScreen = () => { department={formData.department} shortIntro={formData.shortIntro} showDepartmentDropdown={showDepartmentDropdown} + isImageMissing={isImageMissing} + isNameMissing={isNameMissing} + isBasicInfoMissing={isBasicInfoMissing} + isDepartmentMissing={isDepartmentMissing} + isShortIntroMissing={isShortIntroMissing} onSelectImage={handleSelectImage} onClubNameChange={handleClubNameChange} onToggleDepartmentDropdown={handleToggleDepartmentDropdown} @@ -1069,11 +1141,13 @@ const ClubInfoEditScreen = () => { @@ -1095,17 +1169,21 @@ const ClubInfoEditScreen = () => { - - + + void; + required?: boolean; + isMissing?: boolean; + missingText?: string; }; -const ClubSnsInputList = ({ urls, onChange }: Props) => { +const DEFAULT_HELPER_TEXT = "SNS 링크를 1개 이상 입력해주세요 (최대 3개)"; + +const ClubSnsInputList = ({ + urls, + onChange, + required = false, + isMissing = true, + missingText = DEFAULT_HELPER_TEXT, +}: Props) => { + const showError = required && isMissing; + const updateUrl = (index: number, value: string) => { onChange(urls.map((url, urlIndex) => (urlIndex === index ? value : url))); }; @@ -32,7 +45,7 @@ const ClubSnsInputList = ({ urls, onChange }: Props) => { return ( - 동아리 SNS + {required ? "*" : ""}동아리 SNS {urls.map((url, index) => ( // biome-ignore lint/suspicious/noArrayIndexKey: The index is the row identity while editable URLs may be empty or duplicated. @@ -42,7 +55,7 @@ const ClubSnsInputList = ({ urls, onChange }: Props) => { color={Colors.BODYTEXT_DISABLED} /> { SNS 링크 추가 )} - - SNS 링크를 1개 이상 입력해주세요 (최대 3개) - + {isMissing && {missingText}} ); }; @@ -95,6 +106,7 @@ const styles = StyleSheet.create({ fieldLabel: { ...typography.headerXLSemibold, color: Colors.BODYTEXT_SUB, + paddingLeft: s(5), }, inputRow: { flexDirection: "row", @@ -112,6 +124,9 @@ const styles = StyleSheet.create({ borderRadius: 8, backgroundColor: Colors.WHITE, }, + inputError: { + borderColor: Colors.BUTTON_DESTRUCTIVE, + }, removeButton: { width: ms(32), height: ms(32),