diff --git a/sweep/app.json b/sweep/app.json index 86af662..a759c2f 100644 --- a/sweep/app.json +++ b/sweep/app.json @@ -7,6 +7,7 @@ "icon": "./assets/images/icon.png", "scheme": "myapp", "userInterfaceStyle": "automatic", + "owner": "catchb", "splash": { "image": "./assets/images/splash.png", "resizeMode": "contain", @@ -14,14 +15,14 @@ }, "ios": { "supportsTablet": true, - "bundleIdentifier": "com.vietman2.sweep" + "bundleIdentifier": "com.sweepseries.catchb" }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/images/adaptive-icon.png", "backgroundColor": "#ffffff" }, - "package": "com.vietman2.sweep" + "package": "com.sweepseries.catchb" }, "web": { "bundler": "metro", diff --git a/sweep/package.json b/sweep/package.json index 42e06fd..1f6f2b4 100644 --- a/sweep/package.json +++ b/sweep/package.json @@ -4,8 +4,6 @@ "version": "1.0.0", "scripts": { "start": "expo start --tunnel", - "android": "expo start --android", - "ios": "expo start --ios", "web": "expo start --web", "test": "jest --watchAll", "lint": "eslint . --ext .js,.jsx,.ts,.tsx" @@ -52,11 +50,11 @@ "expo-clipboard": "^6.0.3", "expo-config-plugins": "^0.1.2", "expo-constants": "^16.0.2", - "expo-dev-client": "^4.0.28", + "expo-dev-client": "^4.0.29", "expo-font": "~12.0.9", "expo-linear-gradient": "^13.0.2", "expo-linking": "~6.3.1", - "expo-router": "~3.5.23", + "expo-router": "^3.5.23", "expo-splash-screen": "~0.27.5", "expo-status-bar": "~1.12.1", "expo-system-ui": "~3.0.7", @@ -68,6 +66,7 @@ "react-native-calendars": "^1.1307.0", "react-native-collapsible-tab-view": "^8.0.0", "react-native-gesture-handler": "~2.16.1", + "react-native-keyboard-aware-scroll-view": "^0.9.5", "react-native-pager-view": "6.3.0", "react-native-reanimated": "^3.16.1", "react-native-safe-area-context": "4.10.5", diff --git a/sweep/src/app/(tabs)/_layout.tsx b/sweep/src/app/(tabs)/_layout.tsx index be3dff5..d7a813f 100644 --- a/sweep/src/app/(tabs)/_layout.tsx +++ b/sweep/src/app/(tabs)/_layout.tsx @@ -17,7 +17,6 @@ export default function TabLayout() { tabBarActiveTintColor: "#14863E", headerShown: false, }} - detachInactiveScreens={Platform.OS === "ios"} > { + router.back(); + }; + + const BackButton = () => { + return ( + + + + ); + }; + return ( @@ -25,7 +44,30 @@ export default function CalendarLayout() { presentation: "modal", headerShown: true, headerTitle: "일정 추가", - headerShadowVisible: false, + }} + /> + + + + + , + headerTitle: "레슨 상세", }} /> diff --git a/sweep/src/app/(tabs)/calendar/addtodo.tsx b/sweep/src/app/(tabs)/calendar/addtodo.tsx new file mode 100644 index 0000000..5731ad4 --- /dev/null +++ b/sweep/src/app/(tabs)/calendar/addtodo.tsx @@ -0,0 +1,3 @@ +import { AddTodo } from "@pages/calendar"; + +export default AddTodo; diff --git a/sweep/src/app/(tabs)/calendar/daily/[date].tsx b/sweep/src/app/(tabs)/calendar/daily/[date].tsx new file mode 100644 index 0000000..68cc339 --- /dev/null +++ b/sweep/src/app/(tabs)/calendar/daily/[date].tsx @@ -0,0 +1,3 @@ +import { DailySchedule } from "@pages/calendar"; + +export default DailySchedule; diff --git a/sweep/src/app/(tabs)/calendar/lesson/[id].tsx b/sweep/src/app/(tabs)/calendar/lesson/[id].tsx new file mode 100644 index 0000000..408504e --- /dev/null +++ b/sweep/src/app/(tabs)/calendar/lesson/[id].tsx @@ -0,0 +1,3 @@ +import { LessonDetail } from "@pages/calendar"; + +export default LessonDetail; diff --git a/sweep/src/app/(tabs)/calendar/requests.tsx b/sweep/src/app/(tabs)/calendar/requests.tsx new file mode 100644 index 0000000..d1a33e4 --- /dev/null +++ b/sweep/src/app/(tabs)/calendar/requests.tsx @@ -0,0 +1,3 @@ +import { ReservationRequests } from "@pages/calendar"; + +export default ReservationRequests; diff --git a/sweep/src/app/(tabs)/calendar/search.tsx b/sweep/src/app/(tabs)/calendar/search.tsx new file mode 100644 index 0000000..6edc74d --- /dev/null +++ b/sweep/src/app/(tabs)/calendar/search.tsx @@ -0,0 +1,3 @@ +import { CalendarSearch } from "@pages/calendar"; + +export default CalendarSearch; diff --git a/sweep/src/app/(tabs)/front/_layout.tsx b/sweep/src/app/(tabs)/front/_layout.tsx index 3d0cb8e..9c021aa 100644 --- a/sweep/src/app/(tabs)/front/_layout.tsx +++ b/sweep/src/app/(tabs)/front/_layout.tsx @@ -1,14 +1,23 @@ import { Stack } from "expo-router"; -import { HorizontalLogo } from "@components/Icons"; +import { CustomLogo } from "@components/Icons"; +import { useTheme } from "@contexts/theme"; export default function FrontLayout() { + const { theme } = useTheme(); + return ( , + headerLeft: () => ( + + ), headerTitle: "", }} /> diff --git a/sweep/src/app/(tabs)/front/index.tsx b/sweep/src/app/(tabs)/front/index.tsx index 14ac5a5..f63a009 100644 --- a/sweep/src/app/(tabs)/front/index.tsx +++ b/sweep/src/app/(tabs)/front/index.tsx @@ -5,6 +5,7 @@ import { TabBar } from "@components/Tabs"; import { CustomerManagement, EmployeeManagement, + NoticeManagement, ProfileManagement, ProgramManagement, ReviewManagement, @@ -18,7 +19,7 @@ export default function Front() { } + tabBar={(props) => } screenOptions={{ tabBarScrollEnabled: true, }} @@ -50,6 +51,11 @@ export default function Front() { component={EmployeeManagement} options={{ title: "직원관리" }} /> + ); } diff --git a/sweep/src/components/Calendars/CustomDay.tsx b/sweep/src/components/Calendars/CustomDay.tsx index 58d8915..3670631 100644 --- a/sweep/src/components/Calendars/CustomDay.tsx +++ b/sweep/src/components/Calendars/CustomDay.tsx @@ -48,9 +48,15 @@ export function CustomDay({ date, schedules }: Readonly) { backgroundColor: schedule.color, }, ]} - key={schedule.text} + key={schedule.short_text} > - {schedule.text} + + {schedule.short_text} + ))} diff --git a/sweep/src/components/Filters/Filters.tsx b/sweep/src/components/Filters/Filters.tsx index 668e2e2..8e9a791 100644 --- a/sweep/src/components/Filters/Filters.tsx +++ b/sweep/src/components/Filters/Filters.tsx @@ -21,7 +21,7 @@ export function Filters({ onSelect, }: Readonly) { return ( - + {filters.map((filter) => ( onSelect(filter)}> diff --git a/sweep/src/components/Icons/Icons.test.tsx b/sweep/src/components/Icons/Icons.test.tsx index 869aacd..1494d00 100644 --- a/sweep/src/components/Icons/Icons.test.tsx +++ b/sweep/src/components/Icons/Icons.test.tsx @@ -1,5 +1,5 @@ import { AppIcon } from "./AppIcon"; -import { MainLogo, HorizontalLogo } from "./Logo"; +import { CustomLogo, MainLogo, HorizontalLogo } from "./Logo"; import { renderWithProviders } from "@utils/test-utils"; jest.unmock("@components/Icons"); @@ -14,6 +14,14 @@ describe("", () => { }); }); +describe("", () => { + it("renders correctly", () => { + renderWithProviders( + + ); + }); +}); + describe("", () => { it("renders correctly", () => { renderWithProviders(); diff --git a/sweep/src/components/Icons/Logo.tsx b/sweep/src/components/Icons/Logo.tsx index 0969488..d35b4c8 100644 --- a/sweep/src/components/Icons/Logo.tsx +++ b/sweep/src/components/Icons/Logo.tsx @@ -1,4 +1,4 @@ -import { StyleSheet, View } from "react-native"; +import { Image, StyleSheet, Text, View } from "react-native"; import { SvgCssUri } from "react-native-svg/css"; import { useTheme } from "@contexts/theme"; @@ -38,6 +38,21 @@ export function HorizontalLogo({ size = 30 }: Readonly) { ); } +interface CustomProps { + image: string; + text: string; + color: string; +} + +export function CustomLogo({ image, text, color }: Readonly) { + return ( + + + {text} + + ); +} + const styles = StyleSheet.create({ container: { flex: 1, @@ -46,4 +61,17 @@ const styles = StyleSheet.create({ width: "100%", gap: 64, }, + customContainer: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + image: { + width: 30, + height: 30, + }, + customText: { + fontSize: 22, + fontWeight: "bold", + }, }); diff --git a/sweep/src/components/Icons/index.tsx b/sweep/src/components/Icons/index.tsx index 6304b52..00ee741 100644 --- a/sweep/src/components/Icons/index.tsx +++ b/sweep/src/components/Icons/index.tsx @@ -1,6 +1,6 @@ import { SvgCssUri } from "react-native-svg/css"; import { AppIcon } from "./AppIcon"; -import { HorizontalLogo, MainLogo } from "./Logo"; +import { CustomLogo, HorizontalLogo, MainLogo } from "./Logo"; -export { AppIcon, HorizontalLogo, MainLogo, SvgCssUri }; +export { AppIcon, CustomLogo, HorizontalLogo, MainLogo, SvgCssUri }; diff --git a/sweep/src/components/Inputs/TextInput.tsx b/sweep/src/components/Inputs/TextInput.tsx index 9dc605d..9d13e38 100644 --- a/sweep/src/components/Inputs/TextInput.tsx +++ b/sweep/src/components/Inputs/TextInput.tsx @@ -50,14 +50,14 @@ const createStyles = (theme: ThemeColorType) => flexDirection: "row", alignItems: "center", justifyContent: "space-between", - marginVertical: 5, - paddingVertical: 10, + marginVertical: 4, + paddingVertical: 12, borderWidth: 1, borderColor: theme.border, - borderRadius: 5, + borderRadius: 4, }, textinputarea: { flex: 1, - marginHorizontal: 10, + marginHorizontal: 8, }, }); diff --git a/sweep/src/components/Modals/Modals.test.tsx b/sweep/src/components/Modals/Modals.test.tsx new file mode 100644 index 0000000..6313483 --- /dev/null +++ b/sweep/src/components/Modals/Modals.test.tsx @@ -0,0 +1,36 @@ +import { render } from "@testing-library/react-native"; + +import { SimpleModal } from "./SimpleModal"; + +jest.unmock("@components/Modals"); + +describe("", () => { + it("renders correctly", () => { + render( + {}} + onButtonPress={() => {}} + > + <> + + ); + }); + + it("renders large modal correctly", () => { + render( + {}} + onButtonPress={() => {}} + large + > + <> + + ); + }); +}); diff --git a/sweep/src/components/Modals/SimpleModal.tsx b/sweep/src/components/Modals/SimpleModal.tsx new file mode 100644 index 0000000..378746b --- /dev/null +++ b/sweep/src/components/Modals/SimpleModal.tsx @@ -0,0 +1,71 @@ +import { Keyboard, Modal, Pressable, StyleSheet, View } from "react-native"; + +import { TextButton } from "@components/Buttons"; +import { Divider } from "@components/Dividers"; +import { Text } from "@components/Texts"; + +interface Props { + title: string; + buttonText: string; + children: React.ReactNode; + visible: boolean; + hideModal: () => void; + onButtonPress: () => void; + large?: boolean; +} + +export function SimpleModal({ + title, + children, + buttonText, + visible, + hideModal, + onButtonPress, + large = false, +}: Readonly) { + return ( + + + + + {title} + + {children} + + + + + + + ); +} + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: "rgba(0, 0, 0, 0.25)", + }, + modal: { + position: "absolute", + alignSelf: "center", + width: "85%", + backgroundColor: "white", + borderRadius: 10, + }, + textinput: { + paddingHorizontal: 20, + }, + title: { + fontWeight: "bold", + fontSize: 20, + textAlign: "center", + paddingVertical: 7.5, + }, + content: { + flex: 1, + }, + button: { + paddingHorizontal: 20, + marginBottom: 10, + }, +}); diff --git a/sweep/src/components/Modals/index.tsx b/sweep/src/components/Modals/index.tsx new file mode 100644 index 0000000..6aae4bc --- /dev/null +++ b/sweep/src/components/Modals/index.tsx @@ -0,0 +1,3 @@ +import { SimpleModal } from "./SimpleModal"; + +export { SimpleModal }; diff --git a/sweep/src/components/ScrollView/index.tsx b/sweep/src/components/ScrollView/index.tsx index d243fb7..ae1ca71 100644 --- a/sweep/src/components/ScrollView/index.tsx +++ b/sweep/src/components/ScrollView/index.tsx @@ -1,5 +1,5 @@ -import { ScrollView as Scroll } from "react-native"; import { ScrollView as GSScroll } from "react-native-gesture-handler"; +import { KeyboardAwareScrollView as Scroll } from "react-native-keyboard-aware-scroll-view"; import { default as ScrollView } from "./ScrollWithRefresh"; diff --git a/sweep/src/components/Tabs/Tabbar.tsx b/sweep/src/components/Tabs/Tabbar.tsx index 301d57b..16588a3 100644 --- a/sweep/src/components/Tabs/Tabbar.tsx +++ b/sweep/src/components/Tabs/Tabbar.tsx @@ -10,15 +10,21 @@ import { MaterialTopTabBarProps } from "@react-navigation/material-top-tabs"; import { useTheme } from "@contexts/theme"; import { ThemeColorType } from "@themes/colors"; +import { Scroll } from "@components/ScrollView"; const { width: screenWidth } = Dimensions.get("window"); +interface Props extends MaterialTopTabBarProps { + scrollable?: boolean; +} + export function TabBar({ state, descriptors, navigation, position, -}: Readonly) { + scrollable, +}: Readonly) { const { theme } = useTheme(); const styles = createStyles(theme); const tabWidth = screenWidth / state.routes.length; @@ -32,6 +38,72 @@ export function TabBar({ }).start(); }, [state.index, tabWidth]); + if (scrollable) { + return ( + + + {state.routes.map((route, index) => { + const { options } = descriptors[route.key]; + const label = options.title; + const isFocused = state.index === index; + + const onPress = () => { + const event = navigation.emit({ + type: "tabPress", + target: route.key, + canPreventDefault: true, + }); + + if (!isFocused && !event.defaultPrevented) { + navigation.navigate(route.name, route.params); + } + }; + + const onLongPress = () => { + navigation.emit({ + type: "tabLongPress", + target: route.key, + }); + }; + + const inputRange = state.routes.map((_, i) => i); + const opacity = position.interpolate({ + inputRange, + outputRange: inputRange.map((i) => (i === index ? 1 : 0.8)), + }); + + return ( + + + {label} + + + ); + })} + + + ); + } + return ( @@ -130,4 +202,19 @@ const createStyles = (theme: ThemeColorType) => bottom: 0, left: 0, }, + scrollableContainer: { + flexDirection: "row", + backgroundColor: theme.background, + borderBottomWidth: 0.5, + borderBottomColor: theme.lowEmphasis, + }, + scrollableTab: { + alignItems: "center", + paddingVertical: 8, + width: 80, + }, + selectedTab: { + borderBottomWidth: 2, + borderBottomColor: theme.primary, + }, }); diff --git a/sweep/src/components/Tabs/Tabs.test.tsx b/sweep/src/components/Tabs/Tabs.test.tsx index 1b23097..bb7132f 100644 --- a/sweep/src/components/Tabs/Tabs.test.tsx +++ b/sweep/src/components/Tabs/Tabs.test.tsx @@ -1,3 +1,4 @@ +import { configureReanimatedLogger } from "react-native-reanimated"; import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs"; import { NavigationContainer } from "@react-navigation/native"; import { fireEvent, waitFor } from "@testing-library/react-native"; @@ -12,6 +13,10 @@ const Tab = createMaterialTopTabNavigator(); const MockComponent = () => <>; +configureReanimatedLogger({ + strict: false, +}); + describe("", () => { it("renders correctly", () => { const { getByText } = renderWithProviders( @@ -67,4 +72,30 @@ describe("", () => { waitFor(() => fireEvent.press(getByTestId("example2"))); fireEvent(getByTestId("example"), "onLongPress"); }); + + it("renders scrollable correctly", () => { + const { getByTestId } = renderWithProviders( + + } + > + + + + + ); + + waitFor(() => fireEvent.press(getByTestId("example"))); + waitFor(() => fireEvent.press(getByTestId("example2"))); + fireEvent(getByTestId("example"), "onLongPress"); + }); }); diff --git a/sweep/src/components/Texts/Texts.tsx b/sweep/src/components/Texts/Texts.tsx index b59e3a4..ea34cb1 100644 --- a/sweep/src/components/Texts/Texts.tsx +++ b/sweep/src/components/Texts/Texts.tsx @@ -77,6 +77,7 @@ const createStyles = (theme: ThemeColorType) => color: theme.lowEmphasis, }, calloutLarge: { + marginHorizontal: 4, paddingHorizontal: 16, paddingVertical: 8, borderRadius: 8, @@ -84,8 +85,8 @@ const createStyles = (theme: ThemeColorType) => shadowColor: "black", shadowOffset: { width: 2, height: 4 }, shadowOpacity: 0.24, - shadowRadius: 4, - elevation: 4, + shadowRadius: 1, + elevation: 1, }, icon: { position: "absolute", diff --git a/sweep/src/constants/models/calendar/diary.ts b/sweep/src/constants/models/calendar/diary.ts new file mode 100644 index 0000000..8415e7b --- /dev/null +++ b/sweep/src/constants/models/calendar/diary.ts @@ -0,0 +1,5 @@ +export type DiaryType = { + id: number; + date: string; + content: string; +}; diff --git a/sweep/src/constants/models/calendar/index.ts b/sweep/src/constants/models/calendar/index.ts index 3ab07ab..9689719 100644 --- a/sweep/src/constants/models/calendar/index.ts +++ b/sweep/src/constants/models/calendar/index.ts @@ -1,9 +1,14 @@ import { CalendarMemberType, CalendarType } from "./calendar"; -import { ScheduleSimpleType, ScheduleResponseType } from "./schedule"; +import { DiaryType } from "./diary"; +import { ScheduleSimpleType, ScheduleResponseType, LessonDetailType } from "./schedule"; +import { TodoType } from "./todo"; export { CalendarMemberType, CalendarType, + DiaryType, ScheduleSimpleType, ScheduleResponseType, + LessonDetailType, + TodoType, }; diff --git a/sweep/src/constants/models/calendar/schedule.ts b/sweep/src/constants/models/calendar/schedule.ts index b2e1d59..46d669b 100644 --- a/sweep/src/constants/models/calendar/schedule.ts +++ b/sweep/src/constants/models/calendar/schedule.ts @@ -1,8 +1,27 @@ export type ScheduleSimpleType = { - text: string; + id: number; + short_text: string; + time: string; + type: string; + name: string; + detail: string; color: string; + note?: string; }; export type ScheduleResponseType = { [date: string]: ScheduleSimpleType[]; }; + +export type LessonDetailType = { + id: number; + date: string; + time: string; + status: string; + color: string; + program: string; + coach: string; + player: string; + note: string; + feedback: string; +}; diff --git a/sweep/src/constants/models/calendar/todo.ts b/sweep/src/constants/models/calendar/todo.ts new file mode 100644 index 0000000..89641fa --- /dev/null +++ b/sweep/src/constants/models/calendar/todo.ts @@ -0,0 +1,5 @@ +export type TodoType = { + id: number; + text: string; + isDone: boolean; +}; diff --git a/sweep/src/constants/testdata/calendar/diaries.ts b/sweep/src/constants/testdata/calendar/diaries.ts new file mode 100644 index 0000000..fd2c105 --- /dev/null +++ b/sweep/src/constants/testdata/calendar/diaries.ts @@ -0,0 +1,8 @@ +import { DiaryType } from "@models/calendar"; + +export const sampleDiary: DiaryType = { + id: 1, + date: "2024-11-13", + content: + "- 오늘 전체 회의 내용 까먹지 않기\n- 상하체 분리 시퀀스에 대한 이해도 높이기", +}; diff --git a/sweep/src/constants/testdata/calendar/index.ts b/sweep/src/constants/testdata/calendar/index.ts index 8e58a3f..89cf326 100644 --- a/sweep/src/constants/testdata/calendar/index.ts +++ b/sweep/src/constants/testdata/calendar/index.ts @@ -1,4 +1,14 @@ import { sampleCalendars } from "./calendars"; +import { sampleDiary } from "./diaries"; +import { sampleLessons } from "./lessons"; import { sampleSchedules, sampleScheduleResponse } from "./schedules"; +import { sampleTodos } from "./todos"; -export { sampleCalendars, sampleSchedules, sampleScheduleResponse }; +export { + sampleCalendars, + sampleDiary, + sampleLessons, + sampleSchedules, + sampleScheduleResponse, + sampleTodos, +}; diff --git a/sweep/src/constants/testdata/calendar/lessons.ts b/sweep/src/constants/testdata/calendar/lessons.ts new file mode 100644 index 0000000..986f46a --- /dev/null +++ b/sweep/src/constants/testdata/calendar/lessons.ts @@ -0,0 +1,33 @@ +import { LessonDetailType } from "@models/calendar"; + +const sampleLessonDetail: LessonDetailType = { + id: 1, + date: "11월 01일. 화", + time: "오전 9시 ~ 오전 11시 (2시간)", + status: "완료", + color: "#14863E", + program: "엘리트 (고등학생) 1:1 타격레슨", + coach: "홍길동", + player: "김철수", + note: "손목이 덮히는 현상을 고치려고 노력함\n단순히 손목이 덮히는 현상을 스윙 궤도의 변화로 수정하기보다 상하체 분리 후 진행되는 로테이션을 통해 전반적으로 수정함\n\n코치님 강조점:\n1. 랜딩 동작 시 상하체 분리\n2. 오른쪽 상체(팔꿈치) 오픈 수정", + feedback: + "레슨 내용: 상하체 분리 후 로테이션 수정\n레슨:\n- 레그 킥 이후 랜딩 동작 시 상하체 분리가 원활히 되지 않는 상태를 수정하고자 했음\n- 수정 전: 스윙 진행 시 팔꿈치가 오픈되며 배트의 중심점이 뒤에 남아있게 되고 손목이 덮힘\n- 수정 후: 안전한 랜딩 동작 수행 후 상체 돌림 현상이 줄어들고 히트 트랙 분석이 타구 속도 및 발사각이 조정됨", +}; + +const sampleUpcomingLessonDetail: LessonDetailType = { + id: 2, + date: "11월 30일. 토", + time: "오전 9시 ~ 오전 11시 (2시간)", + status: "예정", + color: "#14863E", + program: "엘리트 (고등학생) 1:1 타격레슨", + coach: "홍길동", + player: "김철수", + note: "", + feedback: "", +}; + +export const sampleLessons: LessonDetailType[] = [ + sampleLessonDetail, + sampleUpcomingLessonDetail, +]; diff --git a/sweep/src/constants/testdata/calendar/schedules.ts b/sweep/src/constants/testdata/calendar/schedules.ts index f1ebe1f..8216987 100644 --- a/sweep/src/constants/testdata/calendar/schedules.ts +++ b/sweep/src/constants/testdata/calendar/schedules.ts @@ -2,30 +2,95 @@ import { ScheduleSimpleType, ScheduleResponseType } from "@models/calendar"; export const sampleSchedules: ScheduleSimpleType[] = [ { - text: "길동 레슨 1", + id: 1, + time: "오전 9시 ~ 오전 11시 (2시간)", + type: "레슨", + name: "엘리트 (고등학생) 1:1 타격레슨", + detail: "코치: 홍길동", color: "#14863E", + short_text: "길동 레슨 1", }, { - text: "길동 레슨 2", + id: 2, + time: "오후 3시 ~ 오후 5시 (2시간)", + type: "레슨", + name: "엘리트 (고등학생) 1:1 타격레슨", + detail: "코치: 홍길동", color: "#14863E", + short_text: "길동 레슨 2", + note: "초등학교 6학년 / 포지션은 유격수\n야구한지 6개월 정도 됐는데 학부모가 타격 욕심이 큼", + }, + { + id: 3, + time: "오후 5시 ~ 오후 7시 (2시간)", + type: "일반", + name: "대관", + detail: "", + color: "#FF5833", + short_text: "캐치비 대관", }, ]; export const sampleScheduleResponse: ScheduleResponseType = { "2024-11-09": [ { - text: "길동 레슨 1", + id: 1, + time: "오전 9시 ~ 오전 11시 (2시간)", + type: "레슨", + name: "엘리트 (고등학생) 1:1 타격레슨", + detail: "코치: 홍길동", color: "#14863E", + short_text: "길동 레슨 1", }, ], "2024-11-11": [ { - text: "길동 레슨 2", + id: 2, + time: "오후 3시 ~ 오후 5시 (2시간)", + type: "레슨", + name: "엘리트 (고등학생) 1:1 타격레슨", + detail: "코치: 홍길동", + color: "#14863E", + short_text: "길동 레슨 2", + }, + { + id: 3, + time: "오후 5시 ~ 오후 7시 (2시간)", + type: "레슨", + name: "엘리트 (고등학생) 1:1 타격레슨", + detail: "코치: 홍길동", color: "#14863E", + short_text: "길순 레슨 2", }, + ], + "2024-11-13": [ { - text: "길순 레슨 1", + id: 4, + time: "오후 1시 ~ 오후 3시 (2시간)", + type: "레슨", + name: "엘리트 (고등학생) 1:1 타격레슨", + detail: "코치: 홍길동", color: "#14863E", + short_text: "길동 레슨 3", + note: "초등학교 6학년 / 포지션은 유격수\n야구한지 6개월 정도 됐는데 학부모가 타격 욕심이 큼", + }, + { + id: 5, + time: "오후 3시 ~ 오후 5시 (2시간)", + type: "대관", + name: "캐치비 아카데미 대관", + detail: "", + color: "#FF5833", + short_text: "캐치비 대관", + }, + { + id: 6, + time: "오후 9시 ~ 오후 10시 (1시간)", + type: "일반", + name: "캐치비 베이스볼 코치진 월간 정기 미팅", + detail: "", + color: "#87CEEB", + short_text: "코치진 미팅", }, ], }; diff --git a/sweep/src/constants/testdata/calendar/todos.ts b/sweep/src/constants/testdata/calendar/todos.ts new file mode 100644 index 0000000..41a75f1 --- /dev/null +++ b/sweep/src/constants/testdata/calendar/todos.ts @@ -0,0 +1,19 @@ +import { TodoType } from "@models/calendar"; + +export const sampleTodos: TodoType[] = [ + { + id: 1, + text: "사회인 야구 저녁 8시", + isDone: false, + }, + { + id: 2, + text: "프로젝트 회의", + isDone: true, + }, + { + id: 3, + text: "오더글러브 제작", + isDone: true, + }, +]; diff --git a/sweep/src/fragments/Academy/AcademyCard/AcademyCard.test.tsx b/sweep/src/fragments/Academy/AcademyCard/AcademyCard.test.tsx index d2381fc..07ee7fa 100644 --- a/sweep/src/fragments/Academy/AcademyCard/AcademyCard.test.tsx +++ b/sweep/src/fragments/Academy/AcademyCard/AcademyCard.test.tsx @@ -23,6 +23,11 @@ describe("", () => { describe("", () => { it("renders correctly", () => { - renderWithProviders(); + renderWithProviders( + <> + + + + ); }); }); diff --git a/sweep/src/fragments/Academy/AcademyCard/AcademyCard.tsx b/sweep/src/fragments/Academy/AcademyCard/AcademyCard.tsx index 68ce39d..6bb005d 100644 --- a/sweep/src/fragments/Academy/AcademyCard/AcademyCard.tsx +++ b/sweep/src/fragments/Academy/AcademyCard/AcademyCard.tsx @@ -68,7 +68,12 @@ export function NormalCard({ type = 1 }: Readonly) { ); } -export function ProCard() { +interface PropProps { + num_students: number; + num_requests: number; +} + +export function ProCard({ num_students, num_requests }: Readonly) { const { theme } = useTheme(); const styles = createStyles(theme); @@ -82,12 +87,19 @@ export function ProCard() { 총 수강생 - 0 + {num_students} 예약 승인 요청 - 0 + 0 ? "red" : theme.highEmphasis }, + ]} + > + {num_requests} + diff --git a/sweep/src/fragments/Academy/AcademyProfile/AcademyProfile.test.tsx b/sweep/src/fragments/Academy/AcademyProfile/AcademyProfile.test.tsx index 2047421..7b4831f 100644 --- a/sweep/src/fragments/Academy/AcademyProfile/AcademyProfile.test.tsx +++ b/sweep/src/fragments/Academy/AcademyProfile/AcademyProfile.test.tsx @@ -1,3 +1,5 @@ +import { fireEvent } from "@testing-library/react-native"; + import { AcademyProfile } from "./AcademyProfile"; import { renderWithProviders } from "@utils/test-utils"; @@ -5,8 +7,13 @@ describe("", () => { it("renders correctly", () => { renderWithProviders(); }); - - it("renders pro mode correctly", () => { - renderWithProviders(); + + it("renders pro mode and handles image modal correctly", () => { + const { getByTestId } = renderWithProviders(); + + fireEvent.press(getByTestId("open-modal")); + fireEvent.press(getByTestId("hide")); + fireEvent.press(getByTestId("open-modal")); + fireEvent.press(getByTestId("저장하기")); }); }); diff --git a/sweep/src/fragments/Academy/AcademyProfile/AcademyProfile.tsx b/sweep/src/fragments/Academy/AcademyProfile/AcademyProfile.tsx index 54b18fa..ccb94a8 100644 --- a/sweep/src/fragments/Academy/AcademyProfile/AcademyProfile.tsx +++ b/sweep/src/fragments/Academy/AcademyProfile/AcademyProfile.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { Dimensions, Image, @@ -7,6 +8,7 @@ import { } from "react-native"; import { AppIcon } from "@components/Icons"; +import { SimpleModal } from "@components/Modals"; import { Text } from "@components/Texts"; import { useTheme } from "@contexts/theme"; import { ThemeColorType } from "@themes/colors"; @@ -18,39 +20,68 @@ interface Props { } export function AcademyProfile({ pro }: Readonly) { + const [modalVisible, setModalVisible] = useState(false); + const { theme } = useTheme(); const styles = createStyles(theme); + const hideModal = () => { + setModalVisible(false); + }; + + const openModal = () => { + setModalVisible(true); + }; + + const editProfileImage = async () => { + hideModal(); + }; + return ( - - - {pro && ( - - - 대표 사진 변경 - - )} - - - - Catch B 아카데미 - - - - 인천시 서구 청라한내로 72번길 17, 416호 - + <> + + + {pro && ( + + + 대표 사진 변경 + + )} + - - - {(4.2).toFixed(2)} (42) + + Catch B 아카데미 + + + + 인천시 서구 청라한내로 72번길 17, 416호 + + + + + {(4.2).toFixed(2)} (42) + - + + + + ); } @@ -76,7 +107,7 @@ const createStyles = (theme: ThemeColorType) => bottom: 8, borderRadius: 8, backgroundColor: "#00000050", - zIndex: 1, + zIndex: 200, }, buttonText: { color: "white", diff --git a/sweep/src/fragments/Academy/AcademySimple/AcademySimple.tsx b/sweep/src/fragments/Academy/AcademySimple/AcademySimple.tsx index 86b7945..bae0f58 100644 --- a/sweep/src/fragments/Academy/AcademySimple/AcademySimple.tsx +++ b/sweep/src/fragments/Academy/AcademySimple/AcademySimple.tsx @@ -21,7 +21,9 @@ export function AcademySimple({ academy, quote = false }: Readonly) { - {academy.name} + + {academy.name} + @@ -66,7 +68,9 @@ const createStyles = (theme: ThemeColorType) => justifyContent: "space-between", }, name: { - fontSize: 24, + flex: 1, + paddingRight: 8, + fontSize: 20, fontWeight: "bold", }, rating: { diff --git a/sweep/src/fragments/Academy/Information/Facilities/Facilities.test.tsx b/sweep/src/fragments/Academy/Information/Facilities/Facilities.test.tsx index 878e261..0b2978d 100644 --- a/sweep/src/fragments/Academy/Information/Facilities/Facilities.test.tsx +++ b/sweep/src/fragments/Academy/Information/Facilities/Facilities.test.tsx @@ -1,11 +1,29 @@ +import { fireEvent } from "@testing-library/react-native"; + import { Facilities } from "./Facilities"; import { sampleAcademyDetail } from "@testdata/products"; import { renderWithProviders } from "@utils/test-utils"; describe("", () => { - it("renders correctly", () => { - renderWithProviders( + it("renders equipment", () => { + const { getByTestId } = renderWithProviders( ); + + fireEvent.press(getByTestId("open")); + fireEvent.press(getByTestId("hide")); + fireEvent.press(getByTestId("open")); + fireEvent.press(getByTestId("저장")); + }); + + it("renders services", () => { + const { getByTestId } = renderWithProviders( + + ); + + fireEvent.press(getByTestId("open")); + fireEvent.press(getByTestId("hide")); + fireEvent.press(getByTestId("open")); + fireEvent.press(getByTestId("저장")); }); }); diff --git a/sweep/src/fragments/Academy/Information/Facilities/Facilities.tsx b/sweep/src/fragments/Academy/Information/Facilities/Facilities.tsx index 7ec0d6b..9fa495e 100644 --- a/sweep/src/fragments/Academy/Information/Facilities/Facilities.tsx +++ b/sweep/src/fragments/Academy/Information/Facilities/Facilities.tsx @@ -1,6 +1,9 @@ -import { StyleSheet, Text, View } from "react-native"; +import { useState } from "react"; +import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; import { SvgCssUri } from "react-native-svg/css"; +import { AppIcon } from "@components/Icons"; +import { SimpleModal } from "@components/Modals"; import { useTheme } from "@contexts/theme"; import { FacilityType } from "@models/products"; import { ThemeColorType } from "@themes/colors"; @@ -11,6 +14,9 @@ interface Props { } export function Facilities({ facilities, type }: Readonly) { + //const [options, setOptions] = useState([]); + const [modalVisible, setModalVisible] = useState(false); + const facilitiesToDisplay = facilities.filter( (facility) => facility.type === type ); @@ -18,26 +24,64 @@ export function Facilities({ facilities, type }: Readonly) { const { theme } = useTheme(); const styles = createStyles(theme); + const hideModal = () => { + setModalVisible(false); + }; + + const openModal = () => { + setModalVisible(true); + }; + + const editFacility = async () => { + hideModal(); + }; + return ( - - {facilitiesToDisplay.map((facility) => ( - - - {facility.kor_name} + <> + + + + {type === "구비장비" ? "구비시설" : "편의시설 및 서비스"} + + + + 수정 + - ))} - + + {facilitiesToDisplay.map((facility) => ( + + + {facility.kor_name} + + ))} + + + + + + ); } const createStyles = (theme: ThemeColorType) => StyleSheet.create({ container: { + gap: 8, + }, + facilities: { flexDirection: "row", flexWrap: "wrap", alignItems: "center", @@ -54,4 +98,25 @@ const createStyles = (theme: ThemeColorType) => marginTop: 8, color: theme.lowEmphasis, }, + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + subtitle: { + fontSize: 20, + fontWeight: "bold", + color: theme.highEmphasis, + }, + editButton: { + flexDirection: "row", + alignItems: "center", + gap: 2, + }, + editText: { + fontSize: 16, + color: theme.primary, + textAlignVertical: "center", + }, + modal: {}, }); diff --git a/sweep/src/fragments/Academy/Information/Introduction/Introduction.test.tsx b/sweep/src/fragments/Academy/Information/Introduction/Introduction.test.tsx index 96ec8a2..90e11d4 100644 --- a/sweep/src/fragments/Academy/Information/Introduction/Introduction.test.tsx +++ b/sweep/src/fragments/Academy/Information/Introduction/Introduction.test.tsx @@ -4,12 +4,16 @@ import { Introduction } from "./Introduction"; import { renderWithProviders } from "@utils/test-utils"; describe("", () => { - it("renders correctly", () => { + it("renders and handles modal correctly", () => { const { getByTestId } = renderWithProviders( ); fireEvent.press(getByTestId("expand-button")); fireEvent.press(getByTestId("expand-button")); + fireEvent.press(getByTestId("open")); + fireEvent.press(getByTestId("hide")); + fireEvent.press(getByTestId("open")); + fireEvent.press(getByTestId("저장")); }); }); diff --git a/sweep/src/fragments/Academy/Information/Introduction/Introduction.tsx b/sweep/src/fragments/Academy/Information/Introduction/Introduction.tsx index b7d5fed..43baf5d 100644 --- a/sweep/src/fragments/Academy/Information/Introduction/Introduction.tsx +++ b/sweep/src/fragments/Academy/Information/Introduction/Introduction.tsx @@ -2,50 +2,128 @@ import { useState } from "react"; import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; import { AppIcon } from "@components/Icons"; +import { TextInput } from "@components/Inputs"; +import { SimpleModal } from "@components/Modals"; +import { CalloutSmall } from "@components/Texts"; import { useTheme } from "@contexts/theme"; +import { ThemeColorType } from "@themes/colors"; interface Props { introduction: string; } export function Introduction({ introduction }: Readonly) { + const [introInput, setIntroInput] = useState(introduction); const [expanded, setExpanded] = useState(false); + const [modalVisible, setModalVisible] = useState(false); const { theme } = useTheme(); + const styles = createStyles(theme); + + const hideModal = () => { + setModalVisible(false); + }; + + const openModal = () => { + setModalVisible(true); + }; + + const editIntro = async () => { + hideModal(); + }; return ( - - - {introduction} - - setExpanded(!expanded)} - style={styles.wrapper} - testID="expand-button" + <> + + + 아카데미 소개 + + + 수정 + + + + {introduction} + + setExpanded(!expanded)} + style={styles.wrapper} + testID="expand-button" + > + + + + - - - + + + + + + ); } -const styles = StyleSheet.create({ - text: { - fontSize: 14, - lineHeight: 20, - }, - wrapper: { - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - paddingTop: 16, - }, -}); +const createStyles = (theme: ThemeColorType) => + StyleSheet.create({ + container: { + gap: 8, + }, + text: { + fontSize: 14, + lineHeight: 20, + }, + wrapper: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + paddingTop: 16, + }, + body: { + marginTop: 8, + paddingHorizontal: 16, + gap: 8, + }, + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + subtitle: { + fontSize: 20, + fontWeight: "bold", + color: theme.highEmphasis, + }, + editButton: { + flexDirection: "row", + alignItems: "center", + gap: 2, + }, + editText: { + fontSize: 16, + color: theme.primary, + textAlignVertical: "center", + }, + }); diff --git a/sweep/src/fragments/Academy/Information/WorkingHours/WorkingHours.test.tsx b/sweep/src/fragments/Academy/Information/WorkingHours/WorkingHours.test.tsx index 71b8d5e..dac4555 100644 --- a/sweep/src/fragments/Academy/Information/WorkingHours/WorkingHours.test.tsx +++ b/sweep/src/fragments/Academy/Information/WorkingHours/WorkingHours.test.tsx @@ -1,11 +1,18 @@ +import { fireEvent } from "@testing-library/react-native"; + import { WorkingHours } from "./WorkingHours"; import { sampleAcademyDetail } from "@testdata/products"; import { renderWithProviders } from "@utils/test-utils"; describe("", () => { it("renders correctly", () => { - renderWithProviders( + const { getByTestId } = renderWithProviders( ); + + fireEvent.press(getByTestId("open")); + fireEvent.press(getByTestId("hide")); + fireEvent.press(getByTestId("open")); + fireEvent.press(getByTestId("저장")); }); }); diff --git a/sweep/src/fragments/Academy/Information/WorkingHours/WorkingHours.tsx b/sweep/src/fragments/Academy/Information/WorkingHours/WorkingHours.tsx index 6ddc940..189bd1e 100644 --- a/sweep/src/fragments/Academy/Information/WorkingHours/WorkingHours.tsx +++ b/sweep/src/fragments/Academy/Information/WorkingHours/WorkingHours.tsx @@ -1,5 +1,8 @@ -import { StyleSheet, Text, View } from "react-native"; +import { useState } from "react"; +import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; +import { AppIcon } from "@components/Icons"; +import { SimpleModal } from "@components/Modals"; import { useTheme } from "@contexts/theme"; import { WorkingHoursType } from "@models/products"; import { ThemeColorType } from "@themes/colors"; @@ -9,25 +12,59 @@ interface Props { } export function WorkingHours({ workingHours }: Readonly) { + const [modalVisible, setModalVisible] = useState(false); + const { theme } = useTheme(); const styles = createStyles(theme); + const hideModal = () => { + setModalVisible(false); + }; + + const openModal = () => { + setModalVisible(true); + }; + + const editWorkingHours = async () => { + hideModal(); + }; + return ( - - {workingHours.map((workingHour) => ( - - {workingHour.label} - {workingHour.hours} + <> + + + 운영시간 + + + 수정 + - ))} - + + {workingHours.map((workingHour) => ( + + {workingHour.label} + {workingHour.hours} + + ))} + + + + + + ); } const createStyles = (theme: ThemeColorType) => StyleSheet.create({ container: { - gap: 4, + gap: 16, }, row: { flexDirection: "row", @@ -42,4 +79,28 @@ const createStyles = (theme: ThemeColorType) => fontSize: 14, color: theme.lowEmphasis, }, + hours: { + gap: 4, + }, + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + subtitle: { + fontSize: 20, + fontWeight: "bold", + color: theme.highEmphasis, + }, + editButton: { + flexDirection: "row", + alignItems: "center", + gap: 2, + }, + editText: { + fontSize: 16, + color: theme.primary, + textAlignVertical: "center", + }, + body: {}, }); diff --git a/sweep/src/fragments/Academy/index.tsx b/sweep/src/fragments/Academy/index.tsx index 445681c..78e0871 100644 --- a/sweep/src/fragments/Academy/index.tsx +++ b/sweep/src/fragments/Academy/index.tsx @@ -9,11 +9,13 @@ import { WorkingHours } from "./Information/WorkingHours/WorkingHours"; interface Props { mode: "normal" | "pro"; type?: 1 | 2; + num_students?: number; + num_requests?: number; } -function AcademyCard({ mode, type = 1 }: Readonly) { +function AcademyCard({ mode, type = 1, num_requests = 0, num_students = 0 }: Readonly) { if (mode === "pro") { - return ; + return ; } else { return ; } diff --git a/sweep/src/fragments/Calendar/CalendarButtons/CalendarButtons.test.tsx b/sweep/src/fragments/Calendar/CalendarButtons/CalendarButtons.test.tsx index c60b526..7a7119d 100644 --- a/sweep/src/fragments/Calendar/CalendarButtons/CalendarButtons.test.tsx +++ b/sweep/src/fragments/Calendar/CalendarButtons/CalendarButtons.test.tsx @@ -1,6 +1,7 @@ import { fireEvent } from "@testing-library/react-native"; import { CalendarButtons } from "./CalendarButtons"; +import * as AuthContext from "@contexts/auth"; import { renderWithProviders } from "@utils/test-utils"; jest.mock("expo-router", () => ({ @@ -8,16 +9,48 @@ jest.mock("expo-router", () => ({ push: jest.fn(), }, })); +jest.mock("@contexts/auth", () => ({ + AuthProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), + useAuth: jest.fn(), +})); describe("", () => { - it("renders open correctly and handles buttons", () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(AuthContext, "useAuth").mockReturnValue({ + login: jest.fn(), + logout: jest.fn(), + mode: "normal", + isAuthenticated: true, + }); + }); + + it("renders normal mode correctly and handles buttons", () => { const { getByTestId } = renderWithProviders( ); + fireEvent.press(getByTestId("addtodo")); fireEvent.press(getByTestId("addschedule")); }); + it("renders pro mode correctly and handles buttons", () => { + jest.spyOn(AuthContext, "useAuth").mockReturnValue({ + login: jest.fn(), + logout: jest.fn(), + mode: "pro", + isAuthenticated: true, + }); + + const { getByTestId } = renderWithProviders( + + ); + + fireEvent.press(getByTestId("requests")); + }); + it("renders closed correctly", () => { const { getByTestId } = renderWithProviders( diff --git a/sweep/src/fragments/Calendar/CalendarButtons/CalendarButtons.tsx b/sweep/src/fragments/Calendar/CalendarButtons/CalendarButtons.tsx index cfb1246..227f41c 100644 --- a/sweep/src/fragments/Calendar/CalendarButtons/CalendarButtons.tsx +++ b/sweep/src/fragments/Calendar/CalendarButtons/CalendarButtons.tsx @@ -3,6 +3,7 @@ import { router } from "expo-router"; import { AppIcon } from "@components/Icons"; import { Text } from "@components/Texts"; +import { useAuth } from "@contexts/auth"; import { useTheme } from "@contexts/theme"; import { ThemeColorType } from "@themes/colors"; @@ -12,27 +13,36 @@ interface Props { } export function CalendarButtons({ open, setOpen }: Readonly) { + const { mode } = useAuth(); const { theme } = useTheme(); const styles = createStyles(theme); + const handleTodoPress = () => { + router.push("/calendar/addtodo"); + setOpen(false); + }; + const handleSchedulePress = () => { router.push("/calendar/addschedule"); setOpen(false); }; + const handleRequestPress = () => { + router.push("/calendar/requests"); + setOpen(false); + }; + return ( {open ? ( <> - 메모 - - - - - - 할 일 - + 할 일 추가 + ) { - 일정 + 일정 추가 - - - - - 예약 추가 - - - 예약 승인 - - - - + {mode === "pro" && ( + <> + + 예약 추가 + + + + + + 예약 승인 + + + + + + )} ) : ( ", () => { fireEvent.press(getByTestId("open-color-modal")); fireEvent.press(getByTestId("close-color-modal")); fireEvent.press(getByTestId("open-color-modal")); - fireEvent.press(getByTestId("cancel-color-modal")); + fireEvent.press(getByTestId("select-color-#FF6B6B")); }); }); diff --git a/sweep/src/fragments/Calendar/CalendarOptions/CalendarOptions.tsx b/sweep/src/fragments/Calendar/CalendarOptions/CalendarOptions.tsx index 1231275..787ff44 100644 --- a/sweep/src/fragments/Calendar/CalendarOptions/CalendarOptions.tsx +++ b/sweep/src/fragments/Calendar/CalendarOptions/CalendarOptions.tsx @@ -19,16 +19,35 @@ interface Props { calendar: CalendarType; } +const colorOptions = [ + "#FF6B6B", + "#FFA07A", + "#98FB98", + "#B0E0E6", + "#FFD700", + "#E6E6FA", + "#87CEEB", + "#D8BFD8", +]; + export function CalendarOptions({ calendar }: Readonly) { const [calendarName, setCalendarName] = useState(""); + const [calendarColor, setCalendarColor] = useState(""); + const [nameModalOpen, setNameModalOpen] = useState(false); const [colorModalOpen, setColorModalOpen] = useState(false); const { theme } = useTheme(); const styles = createStyles(theme); + const handleColorSelect = (color: string) => { + setCalendarColor(color); + setColorModalOpen(false); + }; + useEffect(() => { setCalendarName(calendar.title); + setCalendarColor(calendar.color); }, [calendar]); return ( @@ -101,15 +120,27 @@ export function CalendarOptions({ calendar }: Readonly) { style={StyleSheet.absoluteFill} testID="close-color-modal" /> - - 캘린더 색상 - 색상 선택 - setColorModalOpen(false)} - testID="cancel-color-modal" - > - 취소 - + + 캘린더 색상을 선택해주세요. + + {colorOptions.map((color) => ( + handleColorSelect(color)} + testID={`select-color-${color}`} + > + + {calendarColor === color && ( + + )} + + + ))} + @@ -191,4 +222,27 @@ const createStyles = (theme: ThemeColorType) => fontWeight: "bold", color: theme.lowEmphasis, }, + modal2: { + alignItems: "center", + maxWidth: 280, + paddingHorizontal: 32, + paddingTop: 16, + paddingBottom: 24, + gap: 16, + backgroundColor: theme.background, + borderRadius: 16, + }, + colors: { + flexDirection: "row", + flexWrap: "wrap", + justifyContent: "center", + gap: 16, + }, + color: { + justifyContent: "center", + alignItems: "center", + width: 40, + height: 40, + borderRadius: 5, + }, }); diff --git a/sweep/src/fragments/Calendar/CalendarTitle/CalendarTitle.tsx b/sweep/src/fragments/Calendar/CalendarTitle/CalendarTitle.tsx index 566a37b..576b3b3 100644 --- a/sweep/src/fragments/Calendar/CalendarTitle/CalendarTitle.tsx +++ b/sweep/src/fragments/Calendar/CalendarTitle/CalendarTitle.tsx @@ -35,9 +35,11 @@ const styles = StyleSheet.create({ }, character: { fontSize: 20, + fontWeight: "bold", color: "black", }, title: { fontSize: 20, + fontWeight: "600", }, }); diff --git a/sweep/src/fragments/Lesson/LessonHeader/LessonHeader.test.tsx b/sweep/src/fragments/Lesson/LessonHeader/LessonHeader.test.tsx new file mode 100644 index 0000000..126fcfd --- /dev/null +++ b/sweep/src/fragments/Lesson/LessonHeader/LessonHeader.test.tsx @@ -0,0 +1,14 @@ +import { LessonHeader } from "./LessonHeader"; +import { sampleLessons } from "@testdata/calendar"; +import { renderWithProviders } from "@utils/test-utils"; + +describe("", () => { + it("renders correctly", () => { + renderWithProviders( + <> + + + + ); + }); +}); diff --git a/sweep/src/fragments/Lesson/LessonHeader/LessonHeader.tsx b/sweep/src/fragments/Lesson/LessonHeader/LessonHeader.tsx new file mode 100644 index 0000000..229342d --- /dev/null +++ b/sweep/src/fragments/Lesson/LessonHeader/LessonHeader.tsx @@ -0,0 +1,105 @@ +import { StyleSheet, View } from "react-native"; + +import { VerticalDivider } from "@components/Dividers"; +import { Text } from "@components/Texts"; +import { useTheme } from "@contexts/theme"; +import { LessonDetailType } from "@models/calendar"; +import { ThemeColorType } from "@themes/colors"; + +interface Props { + lesson: LessonDetailType; +} + +export function LessonHeader({ lesson }: Readonly) { + const { theme } = useTheme(); + const styles = createStyles(theme); + + return ( + + + {lesson.date} + {lesson.time} + + + + + {lesson.status} + + + + + {lesson.program} + + 코치: {lesson.coach} 수강생: {lesson.player} + + + + + ); +} + +const createStyles = (theme: ThemeColorType) => + StyleSheet.create({ + header: { + gap: 12, + }, + datetime: { + flexDirection: "row", + alignItems: "baseline", + gap: 8, + }, + dateText: { + fontSize: 20, + color: theme.mediumEmphasis, + }, + timeText: { + fontSize: 14, + color: theme.lowEmphasis, + }, + horizontal: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + chip: { + alignItems: "center", + justifyContent: "center", + paddingHorizontal: 8, + paddingVertical: 6, + borderRadius: 4, + borderWidth: 0.5, + }, + chipText: { + fontWeight: "bold", + color: theme.background, + }, + title: { + fontSize: 18, + fontWeight: "bold", + color: theme.highEmphasis, + }, + content: { + gap: 8, + }, + detail: { + fontSize: 14, + color: theme.lowEmphasis, + }, + }); diff --git a/sweep/src/fragments/Lesson/index.tsx b/sweep/src/fragments/Lesson/index.tsx new file mode 100644 index 0000000..846861b --- /dev/null +++ b/sweep/src/fragments/Lesson/index.tsx @@ -0,0 +1,3 @@ +import { LessonHeader } from "./LessonHeader/LessonHeader"; + +export { LessonHeader }; diff --git a/sweep/src/fragments/Schedule/DateTimeHeader/DateTimeHeader.test.tsx b/sweep/src/fragments/Schedule/DateTimeHeader/DateTimeHeader.test.tsx new file mode 100644 index 0000000..34c1357 --- /dev/null +++ b/sweep/src/fragments/Schedule/DateTimeHeader/DateTimeHeader.test.tsx @@ -0,0 +1,56 @@ +import { DateTimeHeader } from "./DateTimeHeader"; +import { renderWithProviders } from "@utils/test-utils"; + +jest.mock("react-native-svg", () => ({ + __esModule: true, + default: "Svg", + Line: "Line", +})); + +describe("", () => { + it("renders correctly", () => { + renderWithProviders( + <> + + + + ); + }); + + it("renders all day mode correctly", () => { + renderWithProviders( + <> + + + + ); + }); +}); diff --git a/sweep/src/fragments/Schedule/DateTimeHeader/DateTimeHeader.tsx b/sweep/src/fragments/Schedule/DateTimeHeader/DateTimeHeader.tsx new file mode 100644 index 0000000..55410e5 --- /dev/null +++ b/sweep/src/fragments/Schedule/DateTimeHeader/DateTimeHeader.tsx @@ -0,0 +1,132 @@ +import { StyleSheet, TouchableOpacity, View } from "react-native"; +import Svg, { Line } from "react-native-svg"; + +import { Text } from "@components/Texts"; +import { useTheme } from "@contexts/theme"; +import { ThemeColorType } from "@themes/colors"; + +interface Props { + selectedStartDateTime: Date; + selectedEndDateTime: Date; + mode: "start" | "end"; + handleStartMode: () => void; + handleEndMode: () => void; + isAllDay: boolean; +} + +export function DateTimeHeader({ + selectedStartDateTime, + selectedEndDateTime, + mode, + handleStartMode, + handleEndMode, + isAllDay, +}: Readonly) { + const { theme } = useTheme(); + const styles = createStyles(theme); + + return ( + + + + {selectedStartDateTime.toLocaleDateString("ko-KR", { + weekday: "short", + year: "numeric", + month: "2-digit", + day: "2-digit", + })} + + {isAllDay ? null : ( + + {selectedStartDateTime.toLocaleTimeString("ko-KR", { + hour: "numeric", + minute: "numeric", + })} + + )} + + + + + + + {selectedEndDateTime.toLocaleDateString("ko-KR", { + weekday: "short", + year: "numeric", + month: "2-digit", + day: "2-digit", + })} + + {isAllDay ? null : ( + + {selectedEndDateTime.toLocaleTimeString("ko-KR", { + hour: "numeric", + minute: "numeric", + })} + + )} + + + ); +} + +const createStyles = (theme: ThemeColorType) => + StyleSheet.create({ + header: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: theme.background, + borderBottomWidth: 1, + borderTopWidth: 1, + borderColor: theme.border, + }, + left: { + flex: 1, + alignItems: "flex-start", + gap: 4, + }, + right: { + flex: 1, + alignItems: "flex-end", + gap: 4, + }, + headerText: { + fontSize: 18, + color: theme.lowEmphasis, + }, + }); diff --git a/sweep/src/fragments/Schedule/ScheduleInput/ScheduleInput.test.tsx b/sweep/src/fragments/Schedule/ScheduleInput/ScheduleInput.test.tsx new file mode 100644 index 0000000..cf2319e --- /dev/null +++ b/sweep/src/fragments/Schedule/ScheduleInput/ScheduleInput.test.tsx @@ -0,0 +1,10 @@ +import { ScheduleInput } from "./ScheduleInput"; +import { renderWithProviders } from "@utils/test-utils"; + +describe("", () => { + it("renders correctly", () => { + renderWithProviders( + + ); + }); +}); diff --git a/sweep/src/fragments/Schedule/ScheduleInput/ScheduleInput.tsx b/sweep/src/fragments/Schedule/ScheduleInput/ScheduleInput.tsx new file mode 100644 index 0000000..e2ce5af --- /dev/null +++ b/sweep/src/fragments/Schedule/ScheduleInput/ScheduleInput.tsx @@ -0,0 +1,41 @@ +import { StyleSheet, TouchableOpacity, View } from "react-native"; + +import { AppIcon } from "@components/Icons"; +import { Text } from "@components/Texts"; +import { useTheme } from "@contexts/theme"; + +interface Props { + icon: string; + text: string; + onPress: () => void; +} + +export function ScheduleInput({ icon, text, onPress }: Readonly) { + const { theme } = useTheme(); + + return ( + + + + {text} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + row: { + flex: 1, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: 8, + paddingVertical: 16, + }, +}); diff --git a/sweep/src/fragments/Schedule/ScheduleSimple/ScheduleSimple.test.tsx b/sweep/src/fragments/Schedule/ScheduleSimple/ScheduleSimple.test.tsx new file mode 100644 index 0000000..d5a7f2b --- /dev/null +++ b/sweep/src/fragments/Schedule/ScheduleSimple/ScheduleSimple.test.tsx @@ -0,0 +1,27 @@ +import { fireEvent } from "@testing-library/react-native"; + +import { ScheduleSimple } from "./ScheduleSimple"; +import { sampleSchedules } from "@testdata/calendar"; +import { renderWithProviders } from "@utils/test-utils"; + +jest.mock("expo-router", () => ({ + router: { + replace: jest.fn(), + }, +})); + +describe("", () => { + it("should render without crashing", () => { + const { getByTestId } = renderWithProviders( + <> + + + + + ); + + fireEvent.press(getByTestId("schedule-simple-1")); + fireEvent.press(getByTestId("schedule-simple-2")); + fireEvent.press(getByTestId("schedule-simple-3")); + }); +}); diff --git a/sweep/src/fragments/Schedule/ScheduleSimple/ScheduleSimple.tsx b/sweep/src/fragments/Schedule/ScheduleSimple/ScheduleSimple.tsx new file mode 100644 index 0000000..96be8b6 --- /dev/null +++ b/sweep/src/fragments/Schedule/ScheduleSimple/ScheduleSimple.tsx @@ -0,0 +1,93 @@ +import { StyleSheet, TouchableOpacity, View } from "react-native"; +import { router } from "expo-router"; + +import { VerticalDivider } from "@components/Dividers"; +import { Text } from "@components/Texts"; +import { useTheme } from "@contexts/theme"; +import { ScheduleSimpleType } from "@models/calendar"; +import { ThemeColorType } from "@themes/colors"; + +interface Props { + schedule: ScheduleSimpleType; +} + +export function ScheduleSimple({ schedule }: Readonly) { + const { theme } = useTheme(); + const styles = createStyles(theme); + + const handlePress = () => { + if (schedule.type === "레슨") { + router.replace({ + pathname: "/calendar/lesson/[id]", + params: { id: schedule.id }, + }); + } + }; + + return ( + + {schedule.time} + + + {schedule.type} + + + + {schedule.name} + {schedule.type === "레슨" && ( + {schedule.detail} + )} + + + {schedule.note && {schedule.note}} + + ); +} + +const createStyles = (theme: ThemeColorType) => + StyleSheet.create({ + container: { + marginBottom: 12, + paddingHorizontal: 8, + paddingVertical: 4, + gap: 8, + }, + time: { + fontSize: 12, + color: theme.lowEmphasis, + }, + horizontal: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + chip: { + alignItems: "center", + justifyContent: "center", + paddingHorizontal: 8, + paddingVertical: 6, + borderRadius: 4, + }, + chipText: { + fontWeight: "bold", + color: theme.background, + }, + title: { + fontSize: 18, + fontWeight: "bold", + color: theme.highEmphasis, + }, + content: { + gap: 4, + }, + detail: { + fontSize: 14, + color: theme.lowEmphasis, + }, + notes: { + marginTop: 8, + fontSize: 14, + lineHeight: 20, + color: theme.lowEmphasis, + }, + }); diff --git a/sweep/src/fragments/Schedule/index.tsx b/sweep/src/fragments/Schedule/index.tsx new file mode 100644 index 0000000..144af0c --- /dev/null +++ b/sweep/src/fragments/Schedule/index.tsx @@ -0,0 +1,5 @@ +import { DateTimeHeader } from "./DateTimeHeader/DateTimeHeader"; +import { ScheduleInput } from "./ScheduleInput/ScheduleInput"; +import { ScheduleSimple } from "./ScheduleSimple/ScheduleSimple"; + +export { DateTimeHeader, ScheduleInput, ScheduleSimple }; diff --git a/sweep/src/fragments/Todo/TodoSimple/TodoSimple.test.tsx b/sweep/src/fragments/Todo/TodoSimple/TodoSimple.test.tsx new file mode 100644 index 0000000..e9d90ac --- /dev/null +++ b/sweep/src/fragments/Todo/TodoSimple/TodoSimple.test.tsx @@ -0,0 +1,18 @@ +import { fireEvent } from "@testing-library/react-native"; + +import { TodoSimple } from "./TodoSimple"; +import { sampleTodos } from "@testdata/calendar"; +import { renderWithProviders } from "@utils/test-utils"; + +describe("", () => { + it("renders correctly and handle toggle", () => { + const { getAllByTestId } = renderWithProviders( + <> + + + + ); + + fireEvent.press(getAllByTestId("toggle")[0]); + }); +}); diff --git a/sweep/src/fragments/Todo/TodoSimple/TodoSimple.tsx b/sweep/src/fragments/Todo/TodoSimple/TodoSimple.tsx new file mode 100644 index 0000000..0e3c81c --- /dev/null +++ b/sweep/src/fragments/Todo/TodoSimple/TodoSimple.tsx @@ -0,0 +1,60 @@ +import { useState } from "react"; +import { StyleSheet, TouchableOpacity, View } from "react-native"; + +import { AppIcon } from "@components/Icons"; +import { Text } from "@components/Texts"; +import { useTheme } from "@contexts/theme"; +import { TodoType } from "@models/calendar"; +import { ThemeColorType } from "@themes/colors"; + +interface Props { + todo: TodoType; +} + +export function TodoSimple({ todo }: Readonly) { + const [isDone, setIsDone] = useState(todo.isDone); + + const { theme } = useTheme(); + const styles = createStyles(theme); + + const handleToggle = () => { + setIsDone((prev) => !prev); + }; + + return ( + + + + + {todo.text} + + ); +} + +const createStyles = (theme: ThemeColorType) => + StyleSheet.create({ + container: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + iconWrapper: { + padding: 2, + borderWidth: 1, + borderColor: theme.primary, + borderRadius: 20, + }, + text: { + fontSize: 14, + color: theme.highEmphasis, + }, + }); diff --git a/sweep/src/fragments/Todo/index.ts b/sweep/src/fragments/Todo/index.ts new file mode 100644 index 0000000..6c697cb --- /dev/null +++ b/sweep/src/fragments/Todo/index.ts @@ -0,0 +1,3 @@ +import { TodoSimple } from "./TodoSimple/TodoSimple"; + +export { TodoSimple }; diff --git a/sweep/src/pages/calendar/AddSchedule/AddSchedule.test.tsx b/sweep/src/pages/calendar/AddSchedule/AddSchedule.test.tsx index 6fa612c..2e6f4f0 100644 --- a/sweep/src/pages/calendar/AddSchedule/AddSchedule.test.tsx +++ b/sweep/src/pages/calendar/AddSchedule/AddSchedule.test.tsx @@ -1,34 +1,53 @@ +import { Platform } from "react-native"; import { fireEvent } from "@testing-library/react-native"; import { AddSchedule } from "./AddSchedule"; import { renderWithProviders } from "@utils/test-utils"; -jest.mock("@react-native-community/datetimepicker", () => { +jest.mock("react-native", () => { + const RN = jest.requireActual("react-native"); + + RN.Platform.OS = "ios"; + + return RN; +}); +jest.mock("@fragments/Schedule", () => { const { TouchableOpacity } = jest.requireActual("react-native"); return { - __esModule: true, - default: ({ - onChange, + DateTimeHeader: ({ + handleEndMode, + handleStartMode, }: { - onChange: (event: any, selectedDate?: Date) => void; - }) => { - return ( - <> - onChange({}, new Date())} - testID="change-datetime" - /> - onChange({})} testID="cancel" /> - - ); - }, - DateTimePickerEvent: jest.fn(), + handleEndMode: () => void; + handleStartMode: () => void; + }) => ( + <> + + + + ), + ScheduleInput: () => null, }; }); describe("", () => { - it("renders correctly", () => { + it("renders correctly (ios)", () => { + Platform.OS = "ios"; + + const { getByTestId } = renderWithProviders(); + + fireEvent.press(getByTestId("start")); + fireEvent.press(getByTestId("change-datetime")); + fireEvent.press(getByTestId("cancel")); + fireEvent.press(getByTestId("end")); + fireEvent.press(getByTestId("change-datetime")); + fireEvent.press(getByTestId("all-day")); + }); + + it("renders correctly (android)", () => { + Platform.OS = "android"; + const { getByTestId } = renderWithProviders(); fireEvent.press(getByTestId("change-datetime")); diff --git a/sweep/src/pages/calendar/AddSchedule/AddSchedule.tsx b/sweep/src/pages/calendar/AddSchedule/AddSchedule.tsx index f68b345..2c72e8f 100644 --- a/sweep/src/pages/calendar/AddSchedule/AddSchedule.tsx +++ b/sweep/src/pages/calendar/AddSchedule/AddSchedule.tsx @@ -1,44 +1,140 @@ -import { useState } from "react"; -import { StyleSheet, View } from "react-native"; +import { useEffect, useState } from "react"; +import { Platform, StyleSheet, TouchableOpacity, View } from "react-native"; import DateTimePicker, { DateTimePickerEvent, } from "@react-native-community/datetimepicker"; +import { TextButton } from "@components/Buttons"; +import { AppIcon } from "@components/Icons"; import { TextInput } from "@components/Inputs"; +import { Scroll } from "@components/ScrollView"; +import { Text } from "@components/Texts"; import { useTheme } from "@contexts/theme"; +import { DateTimeHeader, ScheduleInput } from "@fragments/Schedule"; import { ThemeColorType } from "@themes/colors"; export function AddSchedule() { const [title, setTitle] = useState(""); - const [selectedDateTime, setSelectedDateTime] = useState(new Date()); + const [description, setDescription] = useState(""); + const [selectedStartDateTime, setSelectedStartDateTime] = useState( + new Date() + ); + const [selectedEndDateTime, setSelectedEndDateTime] = useState( + new Date() + ); + const [isAllDay, setIsAllDay] = useState(false); + const [mode, setMode] = useState<"start" | "end">("start"); const { theme } = useTheme(); const styles = createStyles(theme); + const handleEndMode = () => { + setMode("end"); + }; + + const handleStartMode = () => { + setMode("start"); + }; + + const toggleAllDay = () => { + setIsAllDay((prev) => !prev); + }; + const handleDateTimeChange = ( event: DateTimePickerEvent, selectedDate?: Date ) => { - const currentDate = selectedDate || selectedDateTime; - setSelectedDateTime(currentDate); + const currentDate = selectedDate || new Date(); + if (mode === "start") { + setSelectedStartDateTime(currentDate); + } else { + setSelectedEndDateTime(currentDate); + } + }; + + const getDateTimePickerMode = () => { + if (Platform.OS === "ios" && !isAllDay) { + return "datetime"; + } + + return "date"; }; + useEffect(() => { + setSelectedEndDateTime( + new Date(selectedStartDateTime.getTime() + 60 * 60 * 1000) + ); + }, []); + return ( - - - - - + + + + + + + + + + + 종일 + + + + + + + + + + + {}} + /> + {}} /> + {}} /> + {}} /> + + + + {}} /> ); @@ -48,19 +144,49 @@ const createStyles = (theme: ThemeColorType) => StyleSheet.create({ container: { flex: 1, - gap: 16, + paddingBottom: 36, backgroundColor: theme.background, }, + wrapper: { + flex: 1, + }, inputWrapper: { paddingHorizontal: 16, }, pickerWrapper: { alignItems: "center", justifyContent: "center", + marginTop: 16, marginHorizontal: 16, paddingHorizontal: 16, borderRadius: 8, borderWidth: 1, borderColor: theme.border, }, + chip: { + alignItems: "center", + justifyContent: "center", + marginTop: 8, + marginHorizontal: 16, + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 8, + borderWidth: 0.5, + borderColor: theme.primary, + }, + chipText: { + fontSize: 16, + color: theme.primary, + }, + inputs: { + paddingHorizontal: 16, + }, + row: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + buttonContainer: { + paddingHorizontal: 16, + }, }); diff --git a/sweep/src/pages/calendar/AddTodo/AddTodo.test.tsx b/sweep/src/pages/calendar/AddTodo/AddTodo.test.tsx new file mode 100644 index 0000000..a61b65c --- /dev/null +++ b/sweep/src/pages/calendar/AddTodo/AddTodo.test.tsx @@ -0,0 +1,17 @@ +import { fireEvent } from "@testing-library/react-native"; + +import { AddTodo } from "./AddTodo"; +import { renderWithProviders } from "@utils/test-utils"; + +jest.mock("@fragments/Schedule", () => ({ + ScheduleInput: () => <>, +})); + +describe("", () => { + it("renders correctly", () => { + const { getByTestId } = renderWithProviders(); + + fireEvent.press(getByTestId("change-datetime")); + fireEvent.press(getByTestId("cancel")); + }); +}); diff --git a/sweep/src/pages/calendar/AddTodo/AddTodo.tsx b/sweep/src/pages/calendar/AddTodo/AddTodo.tsx new file mode 100644 index 0000000..8225cbe --- /dev/null +++ b/sweep/src/pages/calendar/AddTodo/AddTodo.tsx @@ -0,0 +1,90 @@ +import { useState } from "react"; +import { StyleSheet, View } from "react-native"; +import DateTimePicker, { + DateTimePickerEvent, +} from "@react-native-community/datetimepicker"; + +import { TextButton } from "@components/Buttons"; +import { TextInput } from "@components/Inputs"; +import { useTheme } from "@contexts/theme"; +import { ScheduleInput } from "@fragments/Schedule"; +import { ThemeColorType } from "@themes/colors"; +import { Scroll } from "@components/ScrollView"; + +interface Props { + initialDate?: Date; +} + +export function AddTodo({ initialDate = new Date() }: Readonly) { + const [todo, setTodo] = useState(""); + const [date, setDate] = useState(initialDate); + + const { theme } = useTheme(); + const styles = createStyles(theme); + + const handleDateChange = ( + event: DateTimePickerEvent, + selectedDate?: Date + ) => { + const currentDate = selectedDate || date; + setDate(currentDate); + }; + + return ( + + + + + + + + + + {}} + /> + {}} /> + {}} /> + {}} /> + + + {}} /> + + ); +} + +const createStyles = (theme: ThemeColorType) => + StyleSheet.create({ + container: { + flex: 1, + paddingBottom: 36, + paddingHorizontal: 16, + backgroundColor: theme.background, + }, + contents: { + flex: 1, + }, + wrapper: { + alignItems: "center", + justifyContent: "center", + marginTop: 16, + marginHorizontal: 16, + paddingHorizontal: 16, + borderRadius: 8, + borderWidth: 1, + borderColor: theme.border, + }, + }); diff --git a/sweep/src/pages/calendar/Calendar/Calendar.test.tsx b/sweep/src/pages/calendar/Calendar/Calendar.test.tsx index 4e7f17d..61465ac 100644 --- a/sweep/src/pages/calendar/Calendar/Calendar.test.tsx +++ b/sweep/src/pages/calendar/Calendar/Calendar.test.tsx @@ -22,8 +22,13 @@ jest.mock("@fragments/Calendar", () => ({ })); describe("", () => { - it("renders correctly (month >= 10) and open settings", () => { + beforeEach(() => { + jest.clearAllMocks(); jest.useFakeTimers(); + jest.setSystemTime(new Date("2024-01-01").getTime()); + }); + + it("renders correctly (month >= 10) and open settings", () => { jest.setSystemTime(new Date("2024-10-01").getTime()); const { getByTestId } = renderWithProviders(); @@ -31,13 +36,11 @@ describe("", () => { fireEvent.press(getByTestId("open-settings")); }); - it("renders correctly (month < 10)", () => { - jest.useFakeTimers(); - jest.setSystemTime(new Date("2024-01-01").getTime()); - + it("renders correctly (month < 10) and handles search", () => { const { getByTestId } = renderWithProviders(); fireEvent.press(getByTestId("open-list")); + fireEvent.press(getByTestId("search")); }); it("handles calendar select", () => { @@ -46,4 +49,10 @@ describe("", () => { fireEvent.press(getByTestId("calendar-1")); fireEvent.press(getByTestId("close-buttons")); }); + + it("handles day navigate", () => { + const { getByTestId } = renderWithProviders(); + + fireEvent.press(getByTestId("day-2024-11-01")); + }); }); diff --git a/sweep/src/pages/calendar/Calendar/Calendar.tsx b/sweep/src/pages/calendar/Calendar/Calendar.tsx index 8dab4eb..7cbb713 100644 --- a/sweep/src/pages/calendar/Calendar/Calendar.tsx +++ b/sweep/src/pages/calendar/Calendar/Calendar.tsx @@ -45,6 +45,10 @@ export function Calendar() { ref.current?.close(); }; + const handleSearchPress = () => { + router.push("/calendar/search"); + }; + const handleSettingsPress = () => { router.push({ pathname: "/calendar/settings", @@ -73,7 +77,21 @@ export function Calendar() { const dayComponent = ({ date }: { date: DateData }) => { const schedule = schedules?.[date.dateString]; - return ; + const handleNavigation = () => { + router.push({ + pathname: "/calendar/daily/[date]", + params: { date: date.dateString }, + }); + }; + + return ( + + + + ); }; const renderBackdrop = useCallback( @@ -108,12 +126,17 @@ export function Calendar() { > - - - + + + + + + + + @@ -129,7 +152,7 @@ export function Calendar() { index={-1} enableDynamicSizing backdropComponent={renderBackdrop} - containerStyle={{zIndex: 100}} + containerStyle={{ zIndex: 100 }} > 캘린더 리스트 @@ -140,7 +163,7 @@ export function Calendar() { style={[ styles.calendar, selectedCalendar.id === calendar.id && { - backgroundColor: theme.border, + backgroundColor: theme.backgroundGray, }, ]} onPress={() => handleCalendarSelect(calendar)} @@ -176,6 +199,11 @@ const createStyles = (theme: ThemeColorType) => height: 100, backgroundColor: theme.background, }, + wrapper: { + flexDirection: "row", + alignItems: "center", + gap: 12, + }, sheetContainer: { paddingTop: 8, paddingHorizontal: 24, diff --git a/sweep/src/pages/calendar/CalendarSearch/CalendarSearch.test.tsx b/sweep/src/pages/calendar/CalendarSearch/CalendarSearch.test.tsx new file mode 100644 index 0000000..6592349 --- /dev/null +++ b/sweep/src/pages/calendar/CalendarSearch/CalendarSearch.test.tsx @@ -0,0 +1,30 @@ +import { fireEvent } from "@testing-library/react-native"; +import { router } from "expo-router"; + +import { CalendarSearch } from "./CalendarSearch"; +import { renderWithProviders } from "@utils/test-utils"; + +jest.mock("expo-router", () => ({ + router: { + canGoBack: jest.fn(), + back: jest.fn(), + replace: jest.fn(), + }, +})); + +describe("", () => { + it("renders and handles go back correctly", () => { + jest.spyOn(router, "canGoBack").mockReturnValue(true); + const { getByTestId } = renderWithProviders(); + + fireEvent.press(getByTestId("back")); + }); + + it("handles go back correctly 2", () => { + jest.spyOn(router, "canGoBack").mockReturnValue(false); + + const { getByTestId } = renderWithProviders(); + + fireEvent.press(getByTestId("back")); + }); +}); diff --git a/sweep/src/pages/calendar/CalendarSearch/CalendarSearch.tsx b/sweep/src/pages/calendar/CalendarSearch/CalendarSearch.tsx new file mode 100644 index 0000000..c38ddd7 --- /dev/null +++ b/sweep/src/pages/calendar/CalendarSearch/CalendarSearch.tsx @@ -0,0 +1,63 @@ +import { useState } from "react"; +import { StyleSheet, TouchableOpacity, View } from "react-native"; +import { router } from "expo-router"; + +import { AppIcon } from "@components/Icons"; +import { TextInput } from "@components/Inputs"; +import { useTheme } from "@contexts/theme"; +import { ThemeColorType } from "@themes/colors"; + +export function CalendarSearch() { + const [query, setQuery] = useState(""); + + const { theme } = useTheme(); + const styles = createStyles(theme); + + const handleBackPress = () => { + if (router.canGoBack()) { + router.back(); + } else { + router.replace("/calendar"); + } + }; + + return ( + + + + + + + + + {}} style={styles.wrapper}> + + + + + ); +} + +const createStyles = (theme: ThemeColorType) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.background, + }, + header: { + flexDirection: "row", + alignItems: "center", + paddingTop: 40, + height: 120, + }, + wrapper: { + padding: 16, + }, + inputWrapper: { + flex: 1, + }, + }); diff --git a/sweep/src/pages/calendar/DailySchedule/DailySchedule.test.tsx b/sweep/src/pages/calendar/DailySchedule/DailySchedule.test.tsx new file mode 100644 index 0000000..4f5b601 --- /dev/null +++ b/sweep/src/pages/calendar/DailySchedule/DailySchedule.test.tsx @@ -0,0 +1,40 @@ +import { fireEvent } from "@testing-library/react-native"; +import * as Router from "expo-router"; + +import { DailySchedule } from "./DailySchedule"; +import { renderWithProviders } from "@utils/test-utils"; + +jest.mock("expo-router", () => ({ + useLocalSearchParams: jest.fn(), + router: { + replace: jest.fn(), + }, +})); +jest.mock("@fragments/Schedule", () => ({ + ScheduleSimple: () =>
, +})); +jest.mock("@fragments/Todo", () => ({ + TodoSimple: () =>
, +})); + +describe("", () => { + it("should render no schedule", () => { + jest + .spyOn(Router, "useLocalSearchParams") + .mockReturnValue({ date: "2021-07-02" }); + + const { getByTestId } = renderWithProviders(); + + fireEvent.press(getByTestId("schedule")); + }); + + it("should render schedules and handle edit mode", () => { + jest + .spyOn(Router, "useLocalSearchParams") + .mockReturnValue({ date: "2024-11-09" }); + + const { getByTestId } = renderWithProviders(); + + fireEvent.press(getByTestId("toggle-mode")); + }); +}); diff --git a/sweep/src/pages/calendar/DailySchedule/DailySchedule.tsx b/sweep/src/pages/calendar/DailySchedule/DailySchedule.tsx new file mode 100644 index 0000000..20875ad --- /dev/null +++ b/sweep/src/pages/calendar/DailySchedule/DailySchedule.tsx @@ -0,0 +1,188 @@ +import { useEffect, useState } from "react"; +import { StyleSheet, TextInput, TouchableOpacity, View } from "react-native"; +import { useLocalSearchParams, router } from "expo-router"; +import { StatusBar } from "expo-status-bar"; + +import { Divider } from "@components/Dividers"; +import { AppIcon } from "@components/Icons"; +import { Scroll } from "@components/ScrollView"; +import { Text } from "@components/Texts"; +import { useTheme } from "@contexts/theme"; +import { ScheduleSimple } from "@fragments/Schedule"; +import { TodoSimple } from "@fragments/Todo"; +import { DiaryType, ScheduleSimpleType, TodoType } from "@models/calendar"; +import { + sampleDiary, + sampleScheduleResponse, + sampleTodos, +} from "@testdata/calendar"; +import { ThemeColorType } from "@themes/colors"; + +export function DailySchedule() { + const [schedules, setSchedules] = useState([]); + const [todos, setTodos] = useState([]); + const [diary, setDiary] = useState(); + const [dateObj, setDateObj] = useState(); + + const [diaryEditMode, setDiaryEditMode] = useState(false); + const [diaryContent, setDiaryContent] = useState(""); + + const { date } = useLocalSearchParams<{ date: string }>(); + const { theme } = useTheme(); + const styles = createStyles(theme); + + const handleModeToggle = () => { + setDiaryEditMode(!diaryEditMode); + }; + + const handleSchedulePress = () => { + router.replace("/calendar/addschedule"); + }; + + useEffect(() => { + setSchedules(sampleScheduleResponse[date]); + setTodos(sampleTodos); + setDateObj(new Date(date)); + + if (parseInt(date.split("-")[2]) % 2 === 0) { + setDiary(sampleDiary); + setDiaryContent(sampleDiary.content); + } + }, [date]); + + const isNoSchedule = () => { + if (!schedules) return true; + if (schedules.length === 0) return true; + + return false; + }; + + return ( + + + + {`${dateObj?.toLocaleDateString("ko-KR", { + month: "long", + day: "numeric", + })}. ${dateObj?.toLocaleDateString("ko-KR", { weekday: "short" })}`} + + + + 일정 + + + + + {isNoSchedule() ? ( + <> + 일정이 없습니다. + + + ) : ( + schedules.map((schedule) => ( + + + + + )) + )} + + + + 할 일 + + + + + {todos.map((todo) => ( + + ))} + + + + + 다이어리 + + + + + {diaryEditMode ? ( + + ) : ( + <> + {diary ? ( + + {diary.content} + + ) : ( + 다이어리를 추가해주세요. + )} + + )} + + + + + ); +} + +const createStyles = (theme: ThemeColorType) => + StyleSheet.create({ + container: { + flex: 1, + paddingVertical: 32, + paddingHorizontal: 16, + backgroundColor: theme.background, + }, + wrapper: { + gap: 16, + }, + header: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + title: { + fontSize: 24, + fontWeight: "bold", + color: theme.mediumEmphasis, + }, + content: { + gap: 8, + }, + subtitle: { + fontSize: 20, + fontWeight: "bold", + color: theme.highEmphasis, + }, + emptyText: { + textAlign: "center", + fontSize: 18, + fontWeight: "bold", + color: theme.lowEmphasis, + }, + diary: { + marginTop: 8, + padding: 16, + borderRadius: 8, + borderWidth: 1, + borderColor: theme.primary, + }, + diaryText: { + fontSize: 14, + lineHeight: 20, + }, + textinput: { + padding: 16, + borderWidth: 1, + borderColor: theme.border, + borderRadius: 8, + }, + }); diff --git a/sweep/src/pages/calendar/LessonDetail/LessonDetail.test.tsx b/sweep/src/pages/calendar/LessonDetail/LessonDetail.test.tsx new file mode 100644 index 0000000..158f301 --- /dev/null +++ b/sweep/src/pages/calendar/LessonDetail/LessonDetail.test.tsx @@ -0,0 +1,29 @@ +import * as Router from "expo-router"; + +import { LessonDetail } from "./LessonDetail"; +import { renderWithProviders } from "@utils/test-utils"; + +jest.mock("expo-router", () => ({ + useLocalSearchParams: jest.fn(), +})); +jest.mock("@fragments/Lesson", () => ({ + LessonHeader: () =>
LessonHeader
, +})); + +describe("", () => { + it("renders correctly", () => { + jest + .spyOn(Router, "useLocalSearchParams") + .mockReturnValue({ id: "1" }); + + renderWithProviders(); + }); + + it("renders correctly 2", () => { + jest + .spyOn(Router, "useLocalSearchParams") + .mockReturnValue({ id: "2" }); + + renderWithProviders(); + }); +}); diff --git a/sweep/src/pages/calendar/LessonDetail/LessonDetail.tsx b/sweep/src/pages/calendar/LessonDetail/LessonDetail.tsx new file mode 100644 index 0000000..9ddf9d2 --- /dev/null +++ b/sweep/src/pages/calendar/LessonDetail/LessonDetail.tsx @@ -0,0 +1,107 @@ +import { useEffect, useState } from "react"; +import { StyleSheet, View } from "react-native"; +import { useLocalSearchParams } from "expo-router"; + +import { Divider } from "@components/Dividers"; +import { ErrorPage } from "@components/Fallbacks"; +import { AppIcon } from "@components/Icons"; +import { Text } from "@components/Texts"; +import { useTheme } from "@contexts/theme"; +import { LessonHeader } from "@fragments/Lesson"; +import { LessonDetailType } from "@models/calendar"; +import { sampleLessons } from "@testdata/calendar"; +import { ThemeColorType } from "@themes/colors"; + +export function LessonDetail() { + const [lesson, setLesson] = useState(); + + const { id } = useLocalSearchParams<{ id: string }>(); + + const { theme } = useTheme(); + const styles = createStyles(theme); + + useEffect(() => { + if (parseInt(id) % 2 === 0) { + setLesson(sampleLessons[1]); + } else { + setLesson(sampleLessons[0]); + } + }, [id]); + + if (lesson === undefined) { + return ; + } + + return ( + + + {lesson.status === "완료" ? ( + <> + + {lesson.player} 님의 레슨 노트 + + {lesson.note} + + + + + 코치님 피드백 + + {lesson.feedback} + + + + ) : ( + <> + + + + + 아직 진행되지 않은 일정입니다. + + + + )} + + ); +} + +const createStyles = (theme: ThemeColorType) => + StyleSheet.create({ + container: { + flex: 1, + paddingHorizontal: 24, + paddingVertical: 16, + gap: 16, + backgroundColor: theme.background, + }, + content: { + gap: 8, + }, + subtitle: { + fontSize: 20, + fontWeight: "bold", + color: theme.highEmphasis, + }, + textArea: { + backgroundColor: theme.backgroundGray, + borderRadius: 16, + }, + feedbackText: { + padding: 16, + fontSize: 16, + lineHeight: 24, + color: theme.highEmphasis, + }, + upcoming: { + flex: 1, + alignItems: "center", + justifyContent: "center", + gap: 16, + }, + upcomingText: { + fontSize: 20, + fontWeight: "bold", + color: theme.lowEmphasis, + }, + }); diff --git a/sweep/src/pages/calendar/ReservationRequests/ReservationRequests.test.tsx b/sweep/src/pages/calendar/ReservationRequests/ReservationRequests.test.tsx new file mode 100644 index 0000000..fabc68a --- /dev/null +++ b/sweep/src/pages/calendar/ReservationRequests/ReservationRequests.test.tsx @@ -0,0 +1,8 @@ +import { ReservationRequests } from "./ReservationRequests"; +import { renderWithProviders } from "@utils/test-utils"; + +describe("", () => { + it("renders correctly", () => { + renderWithProviders(); + }); +}); diff --git a/sweep/src/pages/calendar/ReservationRequests/ReservationRequests.tsx b/sweep/src/pages/calendar/ReservationRequests/ReservationRequests.tsx new file mode 100644 index 0000000..3527939 --- /dev/null +++ b/sweep/src/pages/calendar/ReservationRequests/ReservationRequests.tsx @@ -0,0 +1,9 @@ +import { View } from "react-native"; + +export function ReservationRequests() { + return ( + + + + ); +} diff --git a/sweep/src/pages/calendar/Settings/Settings.tsx b/sweep/src/pages/calendar/Settings/Settings.tsx index 8390e72..d58678a 100644 --- a/sweep/src/pages/calendar/Settings/Settings.tsx +++ b/sweep/src/pages/calendar/Settings/Settings.tsx @@ -131,6 +131,7 @@ export function Settings() { value={selectedTime} onChange={handleTimeChange} display="spinner" + locale="ko-KR" /> diff --git a/sweep/src/pages/calendar/index.tsx b/sweep/src/pages/calendar/index.tsx index 20a860d..b36f188 100644 --- a/sweep/src/pages/calendar/index.tsx +++ b/sweep/src/pages/calendar/index.tsx @@ -1,5 +1,19 @@ import { AddSchedule } from "./AddSchedule/AddSchedule"; +import { AddTodo } from "./AddTodo/AddTodo"; import { Calendar } from "./Calendar/Calendar"; +import { CalendarSearch } from "./CalendarSearch/CalendarSearch"; +import { DailySchedule } from "./DailySchedule/DailySchedule"; +import { LessonDetail } from "./LessonDetail/LessonDetail"; +import { ReservationRequests } from "./ReservationRequests/ReservationRequests"; import { Settings } from "./Settings/Settings"; -export { AddSchedule, Calendar, Settings }; +export { + AddSchedule, + AddTodo, + Calendar, + CalendarSearch, + DailySchedule, + LessonDetail, + ReservationRequests, + Settings, +}; diff --git a/sweep/src/pages/front/Notices/Notices.test.tsx b/sweep/src/pages/front/Notices/Notices.test.tsx new file mode 100644 index 0000000..80d24dc --- /dev/null +++ b/sweep/src/pages/front/Notices/Notices.test.tsx @@ -0,0 +1,19 @@ +import { fireEvent } from "@testing-library/react-native"; + +import { NoticeManagement } from "./Notices"; +import { renderWithProviders } from "@utils/test-utils"; + +jest.mock("@fragments/Notice", () => ({ + NoticeSimple: () =>
, +})); + +describe("", () => { + it("should render notice simple", () => { + const { getByTestId } = renderWithProviders(); + + fireEvent.press(getByTestId("open")); + fireEvent.press(getByTestId("hide")); + fireEvent.press(getByTestId("open")); + fireEvent.press(getByTestId("저장")); + }); +}); diff --git a/sweep/src/pages/front/Notices/Notices.tsx b/sweep/src/pages/front/Notices/Notices.tsx new file mode 100644 index 0000000..b933fdc --- /dev/null +++ b/sweep/src/pages/front/Notices/Notices.tsx @@ -0,0 +1,102 @@ +import { useEffect, useState } from "react"; +import { StyleSheet, TouchableOpacity, View } from "react-native"; + +import { Divider } from "@components/Dividers"; +import { AppIcon } from "@components/Icons"; +import { SimpleModal } from "@components/Modals"; +import { Text } from "@components/Texts"; +import { useTheme } from "@contexts/theme"; +import { NoticeSimple } from "@fragments/Notice"; +import { NoticeSimpleType } from "@models/products"; +import { sampleNotices } from "@testdata/products"; +import { ThemeColorType } from "@themes/colors"; + +export function NoticeManagement() { + const [notices, setNotices] = useState([]); + const [modalVisible, setModalVisible] = useState(false); + + const { theme } = useTheme(); + const styles = createStyles(theme); + + const hideModal = () => { + setModalVisible(false); + }; + + const openModal = () => { + setModalVisible(true); + }; + + const postNotice = async () => { + hideModal(); + }; + + useEffect(() => { + setNotices(sampleNotices); + }, []); + + return ( + <> + + + 내 소식 + + + 작성하기 + + + {notices.map((notice) => ( + + + + + ))} + + + + + + ); +} + +const createStyles = (theme: ThemeColorType) => + StyleSheet.create({ + container: { + flex: 1, + padding: 16, + gap: 24, + backgroundColor: theme.background, + }, + header: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + title: { + fontSize: 20, + fontWeight: "bold", + }, + notice: { + gap: 16, + }, + editButton: { + flexDirection: "row", + alignItems: "center", + gap: 2, + }, + editText: { + fontSize: 16, + color: theme.primary, + textAlignVertical: "center", + }, + modal: {}, + }); diff --git a/sweep/src/pages/front/Profile/Profile.tsx b/sweep/src/pages/front/Profile/Profile.tsx index 396612d..3a77c25 100644 --- a/sweep/src/pages/front/Profile/Profile.tsx +++ b/sweep/src/pages/front/Profile/Profile.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Image, StyleSheet, TouchableOpacity, View } from "react-native"; +import { Image, StyleSheet, View } from "react-native"; import { Divider } from "@components/Dividers"; import { AppIcon } from "@components/Icons"; @@ -33,19 +33,15 @@ export function ProfileManagement() { - - - - - + 지도 @@ -58,25 +54,6 @@ export function ProfileManagement() { ); } -interface SubtitleProps { - title: string; -} - -function Subtitle({ title }: Readonly) { - const { theme } = useTheme(); - const styles = createStyles(theme); - - return ( - - {title} - - - 수정 - - - ); -} - const createStyles = (theme: ThemeColorType) => StyleSheet.create({ container: { @@ -87,26 +64,11 @@ const createStyles = (theme: ThemeColorType) => paddingHorizontal: 16, gap: 16, }, - subtitleWrapper: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - }, subtitle: { fontSize: 20, fontWeight: "bold", color: theme.highEmphasis, }, - editButton: { - flexDirection: "row", - alignItems: "center", - gap: 2, - }, - editText: { - fontSize: 16, - color: theme.primary, - textAlignVertical: "center", - }, image: { width: "100%", height: 200, diff --git a/sweep/src/pages/front/index.tsx b/sweep/src/pages/front/index.tsx index fab4048..f428b85 100644 --- a/sweep/src/pages/front/index.tsx +++ b/sweep/src/pages/front/index.tsx @@ -1,5 +1,6 @@ import { CustomerManagement } from "./Customers/Customers"; import { EmployeeManagement } from "./Employees/Employees"; +import { NoticeManagement } from "./Notices/Notices"; import { ProfileManagement } from "./Profile/Profile"; import { ProgramManagement } from "./Programs/Programs"; import { ReviewManagement } from "./Reviews/Reviews"; @@ -7,6 +8,7 @@ import { ReviewManagement } from "./Reviews/Reviews"; export { CustomerManagement, EmployeeManagement, + NoticeManagement, ProfileManagement, ProgramManagement, ReviewManagement, diff --git a/sweep/src/pages/home/AcademyDetail/AcademyDetail.tsx b/sweep/src/pages/home/AcademyDetail/AcademyDetail.tsx index 80ad058..d1ad747 100644 --- a/sweep/src/pages/home/AcademyDetail/AcademyDetail.tsx +++ b/sweep/src/pages/home/AcademyDetail/AcademyDetail.tsx @@ -14,6 +14,7 @@ export function AcademyDetail() { renderHeader={() => } renderTabBar={(props) => } pagerProps={{ scrollEnabled: false }} + headerContainerStyle={{ shadowOpacity: 0 }} initialTabName="정보" > diff --git a/sweep/src/pages/home/AcademyDetail/CoachDetail/CoachDetail.tsx b/sweep/src/pages/home/AcademyDetail/CoachDetail/CoachDetail.tsx index 51223f5..290d7cb 100644 --- a/sweep/src/pages/home/AcademyDetail/CoachDetail/CoachDetail.tsx +++ b/sweep/src/pages/home/AcademyDetail/CoachDetail/CoachDetail.tsx @@ -76,6 +76,7 @@ const createStyles = (theme: ThemeColorType) => StyleSheet.create({ container: { flex: 1, + paddingTop: 24, paddingHorizontal: 16, gap: 16, backgroundColor: theme.background, @@ -97,6 +98,7 @@ const createStyles = (theme: ThemeColorType) => flexDirection: "row", alignItems: "center", justifyContent: "space-between", + marginTop: 8, gap: 8, }, title: { diff --git a/sweep/src/pages/home/Home/Home.test.tsx b/sweep/src/pages/home/Home/Home.test.tsx index f9b8401..6308f4c 100644 --- a/sweep/src/pages/home/Home/Home.test.tsx +++ b/sweep/src/pages/home/Home/Home.test.tsx @@ -57,6 +57,7 @@ describe("", () => { const { getByTestId } = renderWithProviders(); fireEvent.press(getByTestId("academy-1")); + fireEvent.press(getByTestId("academy-detail-1")); }); it("renders pro home correctly", () => { diff --git a/sweep/src/pages/home/Home/Home.tsx b/sweep/src/pages/home/Home/Home.tsx index 5426b43..210c0ef 100644 --- a/sweep/src/pages/home/Home/Home.tsx +++ b/sweep/src/pages/home/Home/Home.tsx @@ -76,7 +76,11 @@ export function Home() { <> {isAuthenticated && ( - + )} {mode === "pro" ? ( @@ -86,7 +90,7 @@ export function Home() { icon="calendar-pointer" /> - + ) : ( @@ -138,7 +142,13 @@ export function Home() { {academies.map((academy) => ( - + handleAcademySelect(academy)} + testID={`academy-detail-${academy.uuid}`} + > + + ))} @@ -268,7 +278,7 @@ const createStyles = (theme: ThemeColorType) => horizontal: { flexDirection: "row", marginTop: 16, - gap: 16, + gap: 8, }, card: { flex: 1, @@ -290,7 +300,7 @@ const createStyles = (theme: ThemeColorType) => color: theme.highEmphasis, }, cardSubtitle: { - fontSize: 16, + fontSize: 14, color: theme.mediumEmphasis, }, cardIcon: { diff --git a/sweep/src/pages/home/MyAcademy/MyAcademy.test.tsx b/sweep/src/pages/home/MyAcademy/MyAcademy.test.tsx index 0a00b79..bb8721b 100644 --- a/sweep/src/pages/home/MyAcademy/MyAcademy.test.tsx +++ b/sweep/src/pages/home/MyAcademy/MyAcademy.test.tsx @@ -1,9 +1,19 @@ +import { fireEvent } from "@testing-library/react-native"; + import { MyAcademy } from "./MyAcademy"; import { renderWithProviders } from "@utils/test-utils"; +jest.mock("expo-router", () => ({ + router: { + push: jest.fn(), + }, +})); jest.mock("@fragments/Academy", () => ({ AcademyCard: () => null, })); +jest.mock("@fragments/Lesson", () => ({ + LessonHeader: () => null, +})); describe("", () => { it("should render correctly (month < 10)", () => { @@ -17,6 +27,8 @@ describe("", () => { jest.spyOn(Date.prototype, "getFullYear").mockReturnValue(2024); jest.spyOn(Date.prototype, "getMonth").mockReturnValue(11); - renderWithProviders(); + const { getByTestId } = renderWithProviders(); + + fireEvent.press(getByTestId("lesson-1")); }); }); diff --git a/sweep/src/pages/home/MyAcademy/MyAcademy.tsx b/sweep/src/pages/home/MyAcademy/MyAcademy.tsx index c2b6a79..d71da94 100644 --- a/sweep/src/pages/home/MyAcademy/MyAcademy.tsx +++ b/sweep/src/pages/home/MyAcademy/MyAcademy.tsx @@ -1,21 +1,34 @@ import { useEffect, useState } from "react"; -import { StyleSheet, View } from "react-native"; +import { StyleSheet, TouchableOpacity, View } from "react-native"; +import { router } from "expo-router"; import { CalendarHeader } from "@components/Calendars"; +import { Divider } from "@components/Dividers"; import { LoadingComponent } from "@components/Fallbacks"; import { Scroll } from "@components/ScrollView"; import { Text } from "@components/Texts"; import { useTheme } from "@contexts/theme"; import { AcademyCard } from "@fragments/Academy"; +import { LessonHeader } from "@fragments/Lesson"; +import { LessonDetailType } from "@models/calendar"; +import { sampleLessons } from "@testdata/calendar"; import { ThemeColorType } from "@themes/colors"; export function MyAcademy() { + const [schedules, setSchedules] = useState([]); const [selectedMonth, setSelectedMonth] = useState(""); const [loading, setLoading] = useState(true); const { theme } = useTheme(); const styles = createStyles(theme); + const handleLessonPress = (lesson: LessonDetailType) => { + router.push({ + pathname: "/calendar/lesson/[id]", + params: { id: lesson.id }, + }); + }; + useEffect(() => { const getCurrentMonth = () => { const currentDate = new Date(); @@ -25,6 +38,7 @@ export function MyAcademy() { }; setSelectedMonth(getCurrentMonth()); + setSchedules(sampleLessons); setLoading(false); }, []); @@ -39,6 +53,17 @@ export function MyAcademy() { selectedMonth={selectedMonth} setSelectedMonth={setSelectedMonth} /> + {schedules.map((schedule) => ( + + handleLessonPress(schedule)} + testID={`lesson-${schedule.id}`} + > + + + + + ))} ); @@ -60,4 +85,8 @@ const createStyles = (theme: ThemeColorType) => marginBottom: 8, marginLeft: 4, }, + lesson: { + paddingVertical: 4, + gap: 8, + }, }); diff --git a/sweep/src/pages/mypage/LikedAcademies/LikedAcademies.tsx b/sweep/src/pages/mypage/LikedAcademies/LikedAcademies.tsx index e7df507..e5a553f 100644 --- a/sweep/src/pages/mypage/LikedAcademies/LikedAcademies.tsx +++ b/sweep/src/pages/mypage/LikedAcademies/LikedAcademies.tsx @@ -33,7 +33,7 @@ export function LikedAcademies() { {academies.map((academy) => ( - + ))} diff --git a/sweep/src/pages/mypage/MyPage/MyPage.test.tsx b/sweep/src/pages/mypage/MyPage/MyPage.test.tsx index b2aea9f..7fa4668 100644 --- a/sweep/src/pages/mypage/MyPage/MyPage.test.tsx +++ b/sweep/src/pages/mypage/MyPage/MyPage.test.tsx @@ -37,10 +37,12 @@ describe("", () => { it("renders correctly when logged in and handles buttons", () => { const { getByTestId } = renderWithProviders(); + fireEvent.press(getByTestId("recent")); fireEvent.press(getByTestId("heart-outline")); fireEvent.press(getByTestId("chatbox-outline")); fireEvent.press(getByTestId("giftbox")); fireEvent.press(getByTestId("lightbulb")); + fireEvent.press(getByTestId("chat")); fireEvent.press(getByTestId("questionmark-circle")); fireEvent.press(getByTestId("bell")); fireEvent.press(getByTestId("person-minus")); @@ -53,7 +55,7 @@ describe("", () => { fireEvent.press(getByTestId("logout")); }); - it("renders correctly and handles logout with no dismiss", () => { + it("renders correctly and handles logout with dismiss", () => { jest.spyOn(router, "canDismiss").mockReturnValue(true); const { getByTestId } = renderWithProviders(); @@ -69,4 +71,14 @@ describe("", () => { }); renderWithProviders(); }); + + it("renders correctly when logged in as pro", () => { + jest.spyOn(AuthContext, "useAuth").mockReturnValue({ + login: jest.fn(), + logout: jest.fn(), + mode: "pro", + isAuthenticated: true, + }); + renderWithProviders(); + }); }); diff --git a/sweep/src/pages/mypage/MyPage/MyPage.tsx b/sweep/src/pages/mypage/MyPage/MyPage.tsx index f5225cd..3f57a42 100644 --- a/sweep/src/pages/mypage/MyPage/MyPage.tsx +++ b/sweep/src/pages/mypage/MyPage/MyPage.tsx @@ -11,7 +11,7 @@ import { MainProfile } from "@fragments/Profile"; import { ThemeColorType } from "@themes/colors"; export function MyPage() { - const { isAuthenticated, logout } = useAuth(); + const { mode, isAuthenticated, logout } = useAuth(); const { theme } = useTheme(); const styles = createStyles(theme); @@ -60,7 +60,11 @@ export function MyPage() { {}} color={theme.primary} backgroundColor={theme.background} diff --git a/sweep/src/pages/start/MainPage/MainPage.tsx b/sweep/src/pages/start/MainPage/MainPage.tsx index c0f6d20..2d2e1be 100644 --- a/sweep/src/pages/start/MainPage/MainPage.tsx +++ b/sweep/src/pages/start/MainPage/MainPage.tsx @@ -84,7 +84,7 @@ const createStyles = (theme: ThemeColorType) => }, image: { flex: 1, - width: 300, + width: "60%", resizeMode: "contain", }, wrapper: { @@ -95,8 +95,8 @@ const createStyles = (theme: ThemeColorType) => button: { alignItems: "center", justifyContent: "center", - width: 200, - height: 200, + width: 160, + height: 160, gap: 8, borderRadius: 30, backgroundColor: theme.background, diff --git a/sweep/src/utils/jest.setup.tsx b/sweep/src/utils/jest.setup.tsx index 1d090a1..b684b9a 100644 --- a/sweep/src/utils/jest.setup.tsx +++ b/sweep/src/utils/jest.setup.tsx @@ -3,6 +3,29 @@ jest.mock("react-native-svg/css", () => ({ SvgCssUri: "SvgCssUri", })); +jest.mock("@react-native-community/datetimepicker", () => { + const { TouchableOpacity } = jest.requireActual("react-native"); + + return { + __esModule: true, + default: ({ + onChange, + }: { + onChange: (event: any, selectedDate?: Date) => void; + }) => { + return ( + <> + onChange({}, new Date())} + testID="change-datetime" + /> + onChange({})} testID="cancel" /> + + ); + }, + DateTimePickerEvent: jest.fn(), + }; +}); jest.mock("@components/Buttons", () => ({ SvgIconButton: ({ icon, onPress }: { icon: string; onPress: () => void }) => { const { TouchableOpacity } = jest.requireActual("react-native"); @@ -59,6 +82,7 @@ jest.mock("@components/Filters", () => { }); jest.mock("@components/Icons", () => ({ AppIcon: () => null, + CustomLogo: () => null, MainLogo: () => null, HorizontalLogo: () => null, })); @@ -98,6 +122,28 @@ jest.mock("@components/Menus", () => { ), }; }); +jest.mock("@components/Modals", () => { + const { TouchableOpacity } = jest.requireActual("react-native"); + return { + SimpleModal: ({ + children, + buttonText, + hideModal, + onButtonPress, + }: { + buttonText: string; + children: React.ReactNode; + hideModal: () => void; + onButtonPress: () => void; + }) => ( + <> + + + {children} + + ), + }; +}); jest.mock("@components/Progressbars", () => ({ Progressbar: () => null, })); diff --git a/sweep/yarn.lock b/sweep/yarn.lock index e16a4b4..a5d46c3 100644 --- a/sweep/yarn.lock +++ b/sweep/yarn.lock @@ -1394,7 +1394,14 @@ pirates "^4.0.6" source-map-support "^0.5.16" -"@babel/runtime@^7.13.10", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.25.0", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.13.10": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.25.0", "@babel/runtime@^7.8.4": version "7.25.7" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz" integrity sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w== @@ -2283,14 +2290,14 @@ "@radix-ui/react-compose-refs@1.0.0": version "1.0.0" - resolved "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae" integrity sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA== dependencies: "@babel/runtime" "^7.13.10" "@radix-ui/react-slot@1.0.1": version "1.0.1" - resolved "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.1.tgz#e7868c669c974d649070e9ecbec0b367ee0b4d81" integrity sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw== dependencies: "@babel/runtime" "^7.13.10" @@ -2648,7 +2655,7 @@ "@react-navigation/bottom-tabs@~6.5.7": version "6.5.20" - resolved "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-6.5.20.tgz" + resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-6.5.20.tgz#5335e75b02c527ef0569bd97d4f9185d65616e49" integrity sha512-ow6Z06iS4VqBO8d7FP+HsGjJLWt2xTWIvuWjpoCvsM/uQXzCRDIjBv9HaKcXbF0yTW7IMir0oDAbU5PFzEDdgA== dependencies: "@react-navigation/elements" "^1.3.30" @@ -2669,7 +2676,7 @@ "@react-navigation/elements@^1.3.30": version "1.3.31" - resolved "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.31.tgz" + resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.3.31.tgz#28dd802a0787bb03fc0e5be296daf1804dbebbcf" integrity sha512-bUzP4Awlljx5RKEExw8WYtif8EuQni2glDaieYROKTnaxsu9kEIA515sXQgUDZU4Ob12VoL7+z70uO3qrlfXcQ== "@react-navigation/material-top-tabs@^6.6.14": @@ -2682,7 +2689,7 @@ "@react-navigation/native-stack@~6.9.12": version "6.9.26" - resolved "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-6.9.26.tgz" + resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-6.9.26.tgz#90facf7783c9927f094bc9f01c613af75b6c241e" integrity sha512-++dueQ+FDj2XkZ902DVrK79ub1vp19nSdAZWxKRgd6+Bc0Niiesua6rMCqymYOVaYh+dagwkA9r00bpt/U5WLw== dependencies: "@react-navigation/elements" "^1.3.30" @@ -3413,14 +3420,14 @@ aggregate-error@^3.0.0: ajv-formats@^2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== dependencies: ajv "^8.0.0" ajv-keywords@^5.1.0: version "5.1.0" - resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== dependencies: fast-deep-equal "^3.1.3" @@ -4315,9 +4322,9 @@ convert-source-map@^2.0.0: integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== cookie-signature@^1.1.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.1.tgz#790dea2cce64638c7ae04d9fabed193bd7ccf3b4" - integrity sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw== + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== cookie@^0.6.0: version "0.6.0" @@ -5218,39 +5225,39 @@ expo-constants@^16.0.2, expo-constants@~16.0.0: "@expo/config" "~9.0.0" "@expo/env" "~0.3.0" -expo-dev-client@^4.0.28: - version "4.0.28" - resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-4.0.28.tgz#3a345662ca2b2dfd7d0fa18e537cb75abd9a563c" - integrity sha512-wz5G4vY3Gbk5GuQTyijdqY4Hwr/NDt5OUTErbOu1vd4XRIAsI+8IkK5hsBUhGmqrdkYnP5NxxOxC/soFzX/9+w== +expo-dev-client@^4.0.29: + version "4.0.29" + resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-4.0.29.tgz#86683c584db6b787828b10e2a049f810a246441d" + integrity sha512-aANlw9dC4PJEPaRNpe+X5xwyYI+aCIcbZklAAsFlkv2/05gLrsvAFgmQpRtowAzF+VggHWde1eKUOeUccAYIEg== dependencies: - expo-dev-launcher "4.0.28" - expo-dev-menu "5.0.22" - expo-dev-menu-interface "1.8.3" + expo-dev-launcher "4.0.29" + expo-dev-menu "5.0.23" + expo-dev-menu-interface "1.8.4" expo-manifests "~0.14.0" expo-updates-interface "~0.16.2" -expo-dev-launcher@4.0.28: - version "4.0.28" - resolved "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-4.0.28.tgz" - integrity sha512-goE7jcaGVA2zu4gV3/hQ9RXqGhUZZAu339VYNLbwPdaNCzFaG6A8MZHg18gytCUnZ5QkRJsYi4q/8YcwUCASlQ== +expo-dev-launcher@4.0.29: + version "4.0.29" + resolved "https://registry.yarnpkg.com/expo-dev-launcher/-/expo-dev-launcher-4.0.29.tgz#c655d842802f696ad6a5e446c60d20c1e453d175" + integrity sha512-0a0SL8mc4FrqPeGxJHe9kf0kG+Di+38Gd+HP5DEL9dcOa8m2qffKnk22UcyujCT6+Qk0OUK1s53nnfqFB26uVw== dependencies: ajv "8.11.0" - expo-dev-menu "5.0.22" + expo-dev-menu "5.0.23" expo-manifests "~0.14.0" resolve-from "^5.0.0" semver "^7.6.0" -expo-dev-menu-interface@1.8.3: - version "1.8.3" - resolved "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-1.8.3.tgz" - integrity sha512-QM0LRozeFT5Ek0N7XpV93M+HMdEKRLEOXn0aW5M3uoUlnqC1+PLtF3HMy3k3hMKTTE/kJ1y1Z7akH07T0lunCQ== +expo-dev-menu-interface@1.8.4: + version "1.8.4" + resolved "https://registry.yarnpkg.com/expo-dev-menu-interface/-/expo-dev-menu-interface-1.8.4.tgz#fa23bf3228ea51cf412599fcb715c27259ffdd84" + integrity sha512-FpYI57EUu9qTSOOi+FZJ58xkCGJK7QD0mTiXK/y1I8lRdZGjCmdBqVvC4dAx2GcbIT78EPxaVf4/90tK/KRK6A== -expo-dev-menu@5.0.22: - version "5.0.22" - resolved "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-5.0.22.tgz" - integrity sha512-VzpdQReAtjbI1qIuwOf0sUzf91HsfGThojgJD9Ez0eca12qY5tTGYzHa1EM9V+zIcNuNZ7+A8bHJJdmZ4zvU6g== +expo-dev-menu@5.0.23: + version "5.0.23" + resolved "https://registry.yarnpkg.com/expo-dev-menu/-/expo-dev-menu-5.0.23.tgz#7e6d6fd93c54ca955e8a69601a0b1991b64389b3" + integrity sha512-ztDvrSdFGkRbMoQlGLyKMS6CslMGylonVW4kQHUrBQApCL0c2NtRwLlr2bA1SXF0S7qYdPPg/ayLnj7DDR5X2w== dependencies: - expo-dev-menu-interface "1.8.3" + expo-dev-menu-interface "1.8.4" semver "^7.5.4" expo-file-system@~17.0.1: @@ -5316,7 +5323,7 @@ expo-modules-core@1.12.26: dependencies: invariant "^2.2.4" -expo-router@~3.5.23: +expo-router@^3.5.23: version "3.5.23" resolved "https://registry.yarnpkg.com/expo-router/-/expo-router-3.5.23.tgz#da038e28c64cb69f19d046d7c651c389c5207a3e" integrity sha512-Re2kYcxov67hWrcjuu0+3ovsLxYn79PuX6hgtYN20MgigY5ttX79KOIBEVGTO3F3y9dxSrGHyy5Z14BcO+usGQ== @@ -8446,7 +8453,7 @@ prompts@^2.0.1, prompts@^2.2.1, prompts@^2.3.2, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@15.8.1, prop-types@^15.5.10, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@15.8.1, prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -8618,6 +8625,19 @@ react-native-helmet-async@2.0.4: react-fast-compare "^3.2.2" shallowequal "^1.1.0" +react-native-iphone-x-helper@^1.0.3: + version "1.3.1" + resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010" + integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg== + +react-native-keyboard-aware-scroll-view@^0.9.5: + version "0.9.5" + resolved "https://registry.yarnpkg.com/react-native-keyboard-aware-scroll-view/-/react-native-keyboard-aware-scroll-view-0.9.5.tgz#e2e9665d320c188e6b1f22f151b94eb358bf9b71" + integrity sha512-XwfRn+T/qBH9WjTWIBiJD2hPWg0yJvtaEw6RtPCa5/PYHabzBaWxYBOl0usXN/368BL1XktnZPh8C2lmTpOREA== + dependencies: + prop-types "^15.6.2" + react-native-iphone-x-helper "^1.0.3" + react-native-pager-view@6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.3.0.tgz#7e734e84b1692f877591335373f6fd92f0d67aa6" @@ -8647,7 +8667,7 @@ react-native-safe-area-context@4.10.5: react-native-screens@3.31.1: version "3.31.1" - resolved "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.31.1.tgz" + resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-3.31.1.tgz#909a890f669e32b0fb1b1410278b71ad2f8238f6" integrity sha512-8fRW362pfZ9y4rS8KY5P3DFScrmwo/vu1RrRMMx0PNHbeC9TLq0Kw1ubD83591yz64gLNHFLTVkTJmWeWCXKtQ== dependencies: react-freeze "^1.0.0" @@ -9117,7 +9137,7 @@ scheduler@^0.23.0: schema-utils@^4.0.1: version "4.2.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== dependencies: "@types/json-schema" "^7.0.9" @@ -9207,9 +9227,9 @@ set-blocking@^2.0.0: integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== set-cookie-parser@^2.4.8: - version "2.7.0" - resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz#ef5552b56dc01baae102acb5fc9fb8cd060c30f9" - integrity sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ== + version "2.7.1" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943" + integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ== set-function-length@^1.2.1: version "1.2.2" @@ -9252,7 +9272,7 @@ shallow-clone@^3.0.0: shallowequal@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== shebang-command@^1.2.0: