Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions apps/mobile/src/features/mypage/profileUpdate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { User } from "@/entities/user";
import { buildUpdatedProfile } from "@/features/mypage/profileUpdate";

describe("buildUpdatedProfile", () => {
it("reflects the saved college and major in the local profile immediately", () => {
const user: User = {
id: "user-id",
nickname: "기존 닉네임",
name: "홍길동",
phone: "01012345678",
email: "user@example.com",
collegeMajor: {
id: 1,
college: "공과대학",
major: "컴퓨터공학부",
},
admissionClass: 23,
};

expect(
buildUpdatedProfile(user, {
nickname: "새 닉네임",
collegeMajorId: 2,
college: "인문대학",
major: "국어국문학과",
admissionClass: 24,
}),
).toEqual({
...user,
nickname: "새 닉네임",
collegeMajor: {
id: 2,
college: "인문대학",
major: "국어국문학과",
},
admissionClass: 24,
});
});
});
23 changes: 23 additions & 0 deletions apps/mobile/src/features/mypage/profileUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { User } from "@/entities/user";

type SavedProfile = {
nickname: string;
collegeMajorId: number;
college: string;
major: string;
admissionClass: number;
};

export const buildUpdatedProfile = (
user: User,
profile: SavedProfile,
): User => ({
...user,
nickname: profile.nickname,
collegeMajor: {
id: profile.collegeMajorId,
college: profile.college,
major: profile.major,
},
admissionClass: profile.admissionClass,
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { SafeAreaView } from "react-native-safe-area-context";
import Toast from "react-native-toast-message";
import CommunityIcon from "react-native-vector-icons/MaterialCommunityIcons";
import Icon from "react-native-vector-icons/MaterialIcons";
import { buildUpdatedProfile } from "@/features/mypage/profileUpdate";
import Header from "@/shared/components/BackHeader";
import { Colors } from "@/shared/constants/colors";
import { typography } from "@/shared/constants/typography";
Expand Down Expand Up @@ -83,8 +84,17 @@ const EditProfileScreen = () => {
admissionClass,
});

const updatedUser = await userService.getUser();
setUser(updatedUser);
if (user) {
setUser(
buildUpdatedProfile(user, {
nickname: name,
collegeMajorId,
college,
major,
admissionClass,
}),
);
}
navigation.goBack();

Toast.show({ type: "info", text1: "프로필이 수정되었어요!" });
Expand Down Expand Up @@ -158,7 +168,7 @@ const EditProfileScreen = () => {
setValue={setMajor}
items={majors?.map((c) => ({ label: c, value: c })) ?? []}
placeholder="학과를 선택해주세요"
style={styles.picker}
style={[styles.picker, styles.majorPicker]}
dropDownContainerStyle={styles.pickerDropdown}
placeholderStyle={styles.pickerPlaceholder}
textStyle={styles.pickerText}
Expand Down Expand Up @@ -289,6 +299,9 @@ const styles = StyleSheet.create({
backgroundColor: Colors.WHITE,
borderRadius: ms(12),
},
majorPicker: {
paddingHorizontal: s(12),
},
pickerDropdown: {
borderWidth: 0,
backgroundColor: Colors.WHITE,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
type RouteProp,
useNavigation,
useRoute,
} from "@react-navigation/native";
import { type RouteProp, useRoute } from "@react-navigation/native";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useContext, useEffect, useState } from "react";
import {
Expand Down Expand Up @@ -52,7 +48,6 @@ type AdminFormErrors = {
const ManageClubRegistrationScreen = () => {
const { clubService } = useContext(serviceContext);
const { user } = useProfile();
const nav = useNavigation();
const queryClient = useQueryClient();
const route =
useRoute<
Expand All @@ -65,14 +60,6 @@ const ManageClubRegistrationScreen = () => {
const isClubRegistrationEditMode = editMode === "clubRegistration";
const isEditMode = editClubId !== undefined && editMode !== undefined;

useEffect(() => {
const parent = nav.getParent();
parent?.setOptions({ tabBarStyle: { display: "none" } });

return () => {
parent?.setOptions({ tabBarStyle: undefined });
};
}, [nav]);
const [formStep, setFormStep] = useState<"form" | "clubSearch">("form");
const [adminForm, setAdminForm] = useState<AdminFormData>({
name: isEditMode ? "" : (user?.name ?? ""),
Expand Down
20 changes: 20 additions & 0 deletions apps/mobile/src/features/mypage/testflightQaUi.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import EditProfileScreen from "@/features/mypage/screens/EditProfileScreen";
import TextField from "@/shared/components/TextField";
import UserVoiceView from "@/shared/components/UserVoiceView";
import { Colors } from "@/shared/constants/colors";
import { vs } from "@/shared/utils/scale";

jest.mock("react", () => {
const actual = jest.requireActual("react");
Expand Down Expand Up @@ -119,6 +120,10 @@ describe("TestFlight QA UI regressions", () => {
zIndex: 1000,
zIndexInverse: 2000,
});
const majorPickerStyle = StyleSheet.flatten(
pickers[1].props.style as TextStyle,
);
expect(majorPickerStyle.paddingHorizontal).toBeGreaterThan(0);
});

it("dismisses the feedback keyboard and uses a readable placeholder", () => {
Expand Down Expand Up @@ -146,6 +151,21 @@ describe("TestFlight QA UI regressions", () => {
expect(dismissSpy).toHaveBeenCalled();
});

it("keeps feedback content and its submit button inside the sheet", () => {
const view = UserVoiceView({ closeBottomSheet: jest.fn() });
const content = view.props.children as ElementWithProps;
const contentStyle = StyleSheet.flatten(content.props.style as TextStyle);
const [input] = findAll(
view,
(element) =>
element.props.placeholder === "여기에 의견을 적어주세요. (1000자 이내)",
);
const inputStyle = StyleSheet.flatten(input.props.style as TextStyle);

expect(contentStyle.flex).toBe(1);
expect(inputStyle.height).toBe(vs(120));
});

it("optically centers single-line text on iOS", () => {
type TextFieldComponent = (
props: TextInputProps & { height?: number },
Expand Down
67 changes: 28 additions & 39 deletions apps/mobile/src/shared/components/UserVoiceView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
} from "react-native";
import Toast from "react-native-toast-message";
import { Colors } from "@/shared/constants/colors";
import { typography } from "@/shared/constants/typography";
import { serviceContext } from "@/shared/contexts/serviceContext";
import { ms, s, vs } from "@/shared/utils/scale";

type Props = {
closeBottomSheet: () => void;
Expand Down Expand Up @@ -79,24 +81,9 @@ const UserVoiceView = ({ closeBottomSheet }: Props) => {
<TouchableOpacity
disabled={!input}
onPress={handleSubmit}
style={[
styles.button,
{
backgroundColor: "#3A3434" /* #deprecated color */,
marginTop: "auto",
},
]}
style={styles.button}
>
<Text
style={{
color: "#FFFFFF", // #deprecated color
fontSize: 16,
textAlign: "center",
fontWeight: "bold",
}}
>
의견 보내기
</Text>
<Text style={styles.buttonText}>의견 보내기</Text>
</TouchableOpacity>
</View>
</View>
Expand All @@ -108,54 +95,56 @@ export default UserVoiceView;

const styles = StyleSheet.create({
mainWrapper: {
display: "flex",
paddingVertical: 32,
paddingHorizontal: 24,
backgroundColor: "white",
flex: 1,
paddingTop: vs(24),
paddingBottom: vs(16),
paddingHorizontal: s(24),
backgroundColor: Colors.WHITE,
},

titleWrapper: {
display: "flex",
flex: 0,
justifyContent: "center",
alignItems: "center",
marginBottom: 24,
marginBottom: vs(24),
},

title: {
fontSize: 16,
lineHeight: 24,
...typography.textInputMedium,
color: Colors.BODYTEXT_MAIN,
},

bold: {
fontWeight: "bold",
...typography.headerL,
},

inputWrapper: {},

input: {
height: "100%",
maxHeight: 120,
backgroundColor: "#FFFFFF", // #deprecated color
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 12,
fontSize: 16,
color: "#3A3434", // #deprecated color
height: vs(120),
backgroundColor: Colors.WHITE,
paddingHorizontal: s(16),
paddingVertical: vs(8),
borderRadius: ms(12),
...typography.textInputMedium,
color: Colors.BODYTEXT_MAIN,
textAlignVertical: "top",
},

buttonWrapper: {
marginTop: "auto",
bottom: 0,
width: "100%",
left: 0,
right: 0,
},

button: {
padding: 16,
borderRadius: 12,
marginBottom: 12,
backgroundColor: Colors.BODYTEXT_MAIN,
padding: s(16),
borderRadius: ms(12),
},
buttonText: {
...typography.headerL,
color: Colors.WHITE,
textAlign: "center",
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { ReactElement, ReactNode } from "react";
import { Platform } from "react-native";
import { UserVoiceBottomSheetProvider } from "@/shared/contexts/userVoiceBottomSheetContext";

jest.mock("react", () => {
const actual = jest.requireActual("react");
return {
...actual,
useCallback: (callback: unknown) => callback,
useEffect: jest.fn(),
useRef: () => ({ current: null }),
};
});

jest.mock("@gorhom/bottom-sheet", () => ({
BottomSheetBackdrop: "BottomSheetBackdrop",
BottomSheetModal: "BottomSheetModal",
BottomSheetView: "BottomSheetView",
BottomSheetTextInput: "BottomSheetTextInput",
}));

jest.mock("@/shared/components/UserVoiceView", () => ({
__esModule: true,
default: "UserVoiceView",
}));

type ElementWithProps = ReactElement<Record<string, unknown>>;

const findAll = (
node: ReactNode,
predicate: (element: ElementWithProps) => boolean,
): ElementWithProps[] => {
if (node == null || typeof node !== "object" || !("props" in node)) {
return [];
}

const element = node as ElementWithProps;
const children = element.props.children;

return [
...(predicate(element) ? [element] : []),
...(Array.isArray(children)
? children.flatMap((child) => findAll(child, predicate))
: findAll(children as ReactNode, predicate)),
];
};

describe("UserVoiceBottomSheetProvider", () => {
it("restores or closes the sheet instead of leaving it partially dragged", () => {
const provider = UserVoiceBottomSheetProvider({ children: null });
const [modal] = findAll(
provider,
(element) => element.type === "BottomSheetModal",
);

expect(modal.props).toMatchObject({
enablePanDownToClose: true,
enableBlurKeyboardOnGesture: true,
keyboardBlurBehavior: "restore",
snapPoints: [Platform.OS === "ios" ? 440 : 420],
});
});
});
Loading
Loading