diff --git a/assets/icons/icon-palette.svg b/assets/icons/icon-palette.svg new file mode 100644 index 00000000..179eee68 --- /dev/null +++ b/assets/icons/icon-palette.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/StudyDataHydrator.tsx b/src/StudyDataHydrator.tsx index cedf5297..a1c166fb 100644 --- a/src/StudyDataHydrator.tsx +++ b/src/StudyDataHydrator.tsx @@ -36,13 +36,8 @@ const StudyDataHydrator = ({ children }: PropsWithChildren) => { const todayRecord = todayStudyRecords.find( record => record.categoryId === studyStateData.categoryId, ); - if (todayRecord == null) { - // eslint-disable-next-line no-console - console.error('Today study record must be defined when starting study session'); - return; - } - const recordedStudyTime = todayRecord.time; + const recordedStudyTime = todayRecord?.time ?? Time.fromMilliseconds(0); const progressiveStudyTime = Time.fromMilliseconds( new Date().getTime() - studyStateData.startTime.getTime(), ); diff --git a/src/components/Icon/IconSet.tsx b/src/components/Icon/IconSet.tsx index 270ebdca..a6f09ae9 100644 --- a/src/components/Icon/IconSet.tsx +++ b/src/components/Icon/IconSet.tsx @@ -32,6 +32,7 @@ import NotiOffOutline from '@icons/icon-noti-off-outline.svg' import SendRight from '@icons/icon-send-right.svg' import TrashBin from '@icons/icon-trash-bin.svg' import Camera from '@icons/icon-camera.svg' +import Palette from '@icons/icon-palette.svg' import type { Icon, MultiIcon } from '@/types/icon' import { SvgProps } from 'react-native-svg' @@ -86,6 +87,7 @@ type IconSetComponent = { SendRight: Icon; TrashBin: Icon; Camera: Icon; + Palette: Icon; } const IconSet: IconSetComponent = { @@ -106,23 +108,24 @@ const IconSet: IconSetComponent = { DogFootprint: withDefaultProps(DogFootprint, { color: theme.color.red[100] }), ArrowLeft: withDefaultProps(ArrowLeft, { color: COLOR_GRAY }), Edit: withDefaultProps(Edit, { color: COLOR_GRAY }), - Study: withDefaultProps(Study, { color: theme.color.neutral['white'] }), - LockFilled: withDefaultProps(LockFilled, { color: theme.color.neutral[500] }), - LockOutline: withDefaultProps(LockOutline, { color: theme.color.neutral[500] }), - UnlockFilled: withDefaultProps(UnlockFilled, { color: theme.color.neutral[500] }), - Invite: withDefaultProps(Invite, { color: theme.color.neutral[500] }), - Graph: withDefaultProps(Graph, { color: theme.color.neutral[500] }), - Ranking: withDefaultProps(Ranking, { color: theme.color.neutral[500] }), - Chat: withDefaultProps(Chat, { color: theme.color.neutral[500] }), - More: withDefaultProps(More, { color: theme.color.neutral[500] }), - Person: withDefaultProps(Person, { color: theme.color.neutral[500] }), - DoorOpened: withDefaultProps(DoorOpened, { color: theme.color.neutral[500] }), - Setting: withDefaultProps(Setting, { color: theme.color.neutral[500] }), - NotiOnFilled: withDefaultProps(NotiOnFilled, { color: theme.color.neutral[500] }), - NotiOffOutline: withDefaultProps(NotiOffOutline, { color: theme.color.neutral[500] }), - SendRight: withDefaultProps(SendRight, { color: theme.color.neutral[500] }), - TrashBin: withDefaultProps(TrashBin, { color: theme.color.neutral[500] }), - Camera: withDefaultProps(Camera, { color: theme.color.neutral['white'] }), + Study: withDefaultProps(Study, { color: COLOR_WHITE }), + LockFilled: withDefaultProps(LockFilled, { color: COLOR_GRAY }), + LockOutline: withDefaultProps(LockOutline, { color: COLOR_GRAY }), + UnlockFilled: withDefaultProps(UnlockFilled, { color: COLOR_GRAY }), + Invite: withDefaultProps(Invite, { color: COLOR_GRAY }), + Graph: withDefaultProps(Graph, { color: COLOR_GRAY }), + Ranking: withDefaultProps(Ranking, { color: COLOR_GRAY }), + Chat: withDefaultProps(Chat, { color: COLOR_GRAY }), + More: withDefaultProps(More, { color: COLOR_GRAY }), + Person: withDefaultProps(Person, { color: COLOR_GRAY }), + DoorOpened: withDefaultProps(DoorOpened, { color: COLOR_GRAY }), + Setting: withDefaultProps(Setting, { color: COLOR_GRAY }), + NotiOnFilled: withDefaultProps(NotiOnFilled, { color: COLOR_GRAY }), + NotiOffOutline: withDefaultProps(NotiOffOutline, { color: COLOR_GRAY }), + SendRight: withDefaultProps(SendRight, { color: COLOR_GRAY }), + TrashBin: withDefaultProps(TrashBin, { color: COLOR_GRAY }), + Camera: withDefaultProps(Camera, { color: COLOR_WHITE }), + Palette: withDefaultProps(Palette, { color: COLOR_GRAY }), }; export default IconSet; diff --git a/src/features/category/hooks/useCategoryColor.ts b/src/features/category/hooks/useCategoryColor.ts new file mode 100644 index 00000000..303e0432 --- /dev/null +++ b/src/features/category/hooks/useCategoryColor.ts @@ -0,0 +1,19 @@ + +import { type GroupColorKey } from '@/constants/groupColors'; +import { useGroupsQuery } from '@/features/groups/api/queries'; +import { useGroupColorKey } from '@/features/groups/hooks/useGroupColorKey'; +import { theme } from '@/theme'; + +const DEFAULT_COLOR_KEY: GroupColorKey = theme.color.primary[500]; + +export const useCategoryColor = (categoryId: number) => { + const { data: groupsData } = useGroupsQuery(); + + const matchingGroupId = + groupsData?.find((g) => g.categoryIds.includes(categoryId))?.groupId ?? null; + + const { colorKey: groupColor } = useGroupColorKey(matchingGroupId); + const colorKey = groupColor ?? DEFAULT_COLOR_KEY; + + return { colorKey }; +}; \ No newline at end of file diff --git a/src/features/groups/hooks/useGroupColorKey.ts b/src/features/groups/hooks/useGroupColorKey.ts new file mode 100644 index 00000000..4e546ea8 --- /dev/null +++ b/src/features/groups/hooks/useGroupColorKey.ts @@ -0,0 +1,50 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query'; + +import type { GroupColorKey } from '@/constants/groupColors'; +import { GROUP_COLORS } from '@/constants/groupColors'; +import { useProfileQuery } from '@/features/user/api/queries'; +import { ASYNC_KEYS, asyncStorage } from '@/utils/asyncStorage'; + +const GROUP_COLOR_KEYS = Object.keys(GROUP_COLORS) as GroupColorKey[]; + +const pickRandomGroupColorKey = (): GroupColorKey => + GROUP_COLOR_KEYS[Math.floor(Math.random() * GROUP_COLOR_KEYS.length)]; + +const isGroupColorKey = (v: string): v is GroupColorKey => + (GROUP_COLOR_KEYS as readonly string[]).includes(v); + +export const useGroupColorKey = (groupId: number | null) => { + const { data: profileData } = useProfileQuery(); + const userId = profileData?.id; + const qc = useQueryClient(); + + const queryKey = ['groupColor', userId, groupId]; + + const { data: colorKey } = useQuery({ + queryKey, + enabled: userId != null && groupId != null, + queryFn: async () => { + if (userId == null || groupId == null) return null; + + const storageKey = ASYNC_KEYS.groupColor(userId, groupId); + const stored = await asyncStorage.get(storageKey); + + if (stored && isGroupColorKey(stored)) return stored; + + const newKey = pickRandomGroupColorKey(); + await asyncStorage.set(storageKey, newKey); + return newKey; + }, + staleTime: Infinity, + gcTime: Infinity, + }); + + const updateColorKey = async (next: GroupColorKey) => { + if (userId == null || groupId == null) return; + + qc.setQueryData(queryKey, next); + await asyncStorage.set(ASYNC_KEYS.groupColor(userId, groupId), next); + }; + + return { colorKey: colorKey ?? null, updateColorKey }; +}; diff --git a/src/features/study/api/mutations.ts b/src/features/study/api/mutations.ts index bae43475..74e4d90e 100644 --- a/src/features/study/api/mutations.ts +++ b/src/features/study/api/mutations.ts @@ -26,6 +26,7 @@ export const useStartStudyMutation = () => { studyStatByDateQueryOptions(new Date()), ); // TODO: 24시간이 넘어가는 공부시간에 대해 어떻게 처리할 것인지? + // TODO: StudyDataHydrator와 중복되는 로직 리팩토링 const category = studyDataUtils.identifyCategory({ categories: categories.data, categoryId: response.data.categoryId, @@ -35,13 +36,7 @@ export const useStartStudyMutation = () => { const todayRecord = todayStudyRecords.find( record => record.categoryId === response.data.categoryId, ); - if (todayRecord == null) { - // eslint-disable-next-line no-console - console.error('Today study record must be defined when starting study session'); - return; - } - - const recordedStudyTime = todayRecord.time; + const recordedStudyTime = todayRecord?.time ?? Time.fromMilliseconds(0); const progressiveStudyTime = Time.fromMilliseconds( new Date().getTime() - response.data.startTime.getTime(), ); diff --git a/src/layout/StackableScreenLayout/index.tsx b/src/layout/StackableScreenLayout/index.tsx index ca716b36..b6f3653c 100644 --- a/src/layout/StackableScreenLayout/index.tsx +++ b/src/layout/StackableScreenLayout/index.tsx @@ -24,7 +24,7 @@ const StackableScreenLayout = ({ headerElementColor = theme.color.primary[500], header, children, - safeAreas, + safeAreas = 'top', ...screenContainerProps }: StackableScreenProps) => { const navigation = useStackNavigation(); diff --git a/src/locales/ko/groupDetail.json b/src/locales/ko/groupDetail.json index 0e3a16f6..118e320a 100644 --- a/src/locales/ko/groupDetail.json +++ b/src/locales/ko/groupDetail.json @@ -3,7 +3,7 @@ "public-group": "공개 그룹", "private-group": "비공개 그룹", "ranking": "랭킹", - "statistics": "통계", + "color": "색상", "chat": "채팅", "setting": "설정", "member-list": "멤버 목록", @@ -21,5 +21,6 @@ "cannot-join-your-group": "본인이 속한 그룹에는 가입할 수 없어요.", "group-join-success": "그룹 가입이 완료되었습니다.", "group-join-request-sent": "그룹 참여 요청이 전송되었습니다.", - "failed-to-join-group": "그룹 가입에 실패했습니다. 다시 시도해주세요." + "failed-to-join-group": "그룹 가입에 실패했습니다. 다시 시도해주세요.", + "group-color-picker-title": "그룹 테마 색상 선택" } \ No newline at end of file diff --git a/src/screens/bottomTab/GroupsScreen/components/JoinedGroupPanel/index.tsx b/src/screens/bottomTab/GroupsScreen/components/JoinedGroupPanel/index.tsx index c485559c..e51f0d3b 100644 --- a/src/screens/bottomTab/GroupsScreen/components/JoinedGroupPanel/index.tsx +++ b/src/screens/bottomTab/GroupsScreen/components/JoinedGroupPanel/index.tsx @@ -2,6 +2,8 @@ import { GroupButton } from '@features/groups/ui'; import { Button, Icon, Panel } from '@/components'; import { useGroupsQuery } from '@/features/groups/api/queries'; +import { useGroupColorKey } from '@/features/groups/hooks/useGroupColorKey'; +import type { GetGroupsResponse } from '@/features/groups/model/groups'; import { useStackNavigation } from '@/hooks'; import { useTranslatedText } from '@/hooks/useTranslatedText'; import { theme } from '@/theme'; @@ -14,34 +16,14 @@ const JoinedGroupPanel = () => { const tCreateNewGroup = useTranslatedText({ tKey: 'groups.create-new-group' }); const navigation = useStackNavigation(); const { data, isPending } = useGroupsQuery(); - + if (!data || isPending) return null; return ( {data.length ? - data.map(group => { - const leaderImageUrl = - group.profiles.find(profile => profile.role === 'LEADER')?.imageUrl ?? null; - const otherImageUrls = group.profiles - .filter(profile => profile.role !== 'LEADER') - .map(profile => profile.imageUrl); - return ( - - ); - }) + data.map(group => ) : } { ); }; +const GroupListItem = ({ group }: { group: GetGroupsResponse['data'][number] }) => { + const { colorKey: groupColorKey } = useGroupColorKey(group.groupId); + if (!groupColorKey) return null; + + const leaderImageUrl = + group.profiles.find(p => p.role === 'LEADER')?.imageUrl ?? null; + + const otherImageUrls = group.profiles + .filter(p => p.role !== 'LEADER') + .map(p => p.imageUrl); + + return ( + + ); +}; + export default JoinedGroupPanel; diff --git a/src/screens/bottomTab/HomeScreen/components/StudyBottomSheet/core/GoalItemList.tsx b/src/screens/bottomTab/HomeScreen/components/StudyBottomSheet/core/GoalItemList.tsx index 0f285c85..b7bcf499 100644 --- a/src/screens/bottomTab/HomeScreen/components/StudyBottomSheet/core/GoalItemList.tsx +++ b/src/screens/bottomTab/HomeScreen/components/StudyBottomSheet/core/GoalItemList.tsx @@ -1,11 +1,13 @@ import { startOfDay } from 'date-fns'; -import type { GroupColorKey } from '@/constants/groupColors'; import { useCategoriesQuery } from '@/features/category/api/queries'; +import { useCategoryColor } from '@/features/category/hooks/useCategoryColor'; +import type { GetCategoriesResponse } from '@/features/category/model'; import type { DAY_MASK_BY_UTC_CODE } from '@/features/category/model/types'; import { CategoryProgressItem } from '@/features/category/ui'; import { useStartStudyMutation } from '@/features/study/api/mutations'; import { useStudyStatByDateQuery } from '@/features/study/api/queries'; +import type { GetStudyStatByDateResponse } from '@/features/study/model'; import { formatDateToHMS, getDayByUTCCode } from '@/utils/date'; import { Time } from '@/utils/Time'; @@ -22,8 +24,6 @@ const GoalItemList = ({ }: GoalItemListProps) => { const { data: categoryData, isPending: isCategoryDataPending } = useCategoriesQuery(); const { data: studyData, isPending: isStudyDataPending } = useStudyStatByDateQuery(selectedDate); - - const { startStudyMutate } = useStartStudyMutation(); if (!categoryData || isCategoryDataPending) return null; if (!studyData || isStudyDataPending) return null; @@ -37,38 +37,61 @@ const GoalItemList = ({ getDayByUTCCode(selectedDate.getDay() as keyof typeof DAY_MASK_BY_UTC_CODE) ]; }) - .map(category => { - const onStudyButtonPress = () => { - startStudyMutate({ - categoryId: category.id, - // TODO: 임시 카테고리 기능 구현 후 수정 필요 - temporaryName: null, - startTime: formatDateToHMS(new Date()), - }, { - onSuccess: () => { - setBottomSheetEnabled(false); - }, - }); - }; - const studyTime = - studyData.find(stat => stat.categoryId === category.id)?.time ?? Time.fromSeconds(0); + .map(category => + ); + + return ( + categoryNodes.length ? categoryNodes : + ); +}; - return ( - - ); +const GoalItemListElement = ({ + category, + studyData, + setBottomSheetEnabled, + selectedDate, +}: { + category: GetCategoriesResponse['data'][number]; + studyData: GetStudyStatByDateResponse['data']; + setBottomSheetEnabled: (enabled: boolean) => void; + selectedDate: Date; +}) => { + const { startStudyMutate } = useStartStudyMutation(); + const { colorKey: categoryColor } = useCategoryColor(category.id); + + const onStudyButtonPress = () => { + startStudyMutate({ + categoryId: category.id, + // TODO: 임시 카테고리 기능 구현 후 수정 필요 + temporaryName: null, + startTime: formatDateToHMS(new Date()), + }, { + onSuccess: () => { + setBottomSheetEnabled(false); + }, }); + }; + const studyTime = studyData.find( + stat => stat.categoryId === category.id, + )?.time ?? Time.fromSeconds(0); return ( - categoryNodes.length ? categoryNodes : + ); }; diff --git a/src/screens/bottomTab/StatsScreen/components/SelectedDateStat/index.tsx b/src/screens/bottomTab/StatsScreen/components/SelectedDateStat/index.tsx index a7798ce9..2bbce398 100644 --- a/src/screens/bottomTab/StatsScreen/components/SelectedDateStat/index.tsx +++ b/src/screens/bottomTab/StatsScreen/components/SelectedDateStat/index.tsx @@ -2,10 +2,12 @@ import { View } from 'react-native'; import { TText } from '@/components'; import { useCategoriesQuery } from '@/features/category/api/queries'; +import { useCategoryColor } from '@/features/category/hooks/useCategoryColor'; +import type { GetCategoriesResponse } from '@/features/category/model'; import CategoryProgressItem from '@/features/category/ui/CategoryProgressItem'; import { useStudyStatByDateQuery } from '@/features/study/api/queries'; +import type { GetStudyStatByDateResponse } from '@/features/study/model'; import { studyDataUtils } from '@/store/studySlice'; -import { theme } from '@/theme'; import { Time } from '@/utils/Time'; import { SELECTED_DATE_HEADER_TITLE_TEXT_STYLE, TOTAL_STUDY_TIME_TEXT_STYLE } from './index.style'; @@ -46,28 +48,39 @@ const SelectedDateStat = ({ selectedDate }: SelectedDateStatProps) => { }), }} /> - { - studyStatData.map(studyStat => { - const categoryInfo = (studyStat.categoryId) ? - studyDataUtils.getCategoryById(categoryData, studyStat.categoryId) - : studyDataUtils.getCategoryByTemporaryName(categoryData, studyStat.name!); - return ( - // TODO: groupColorKey 설정하기 - - ); - }) - } + {studyStatData.map(studyStat => + )} ); }; +const Item = ({ + studyStat, + categoryData, +}: { + studyStat: GetStudyStatByDateResponse['data'][number]; + categoryData: GetCategoriesResponse['data']; +}) => { + const categoryInfo = (studyStat.categoryId) ? + studyDataUtils.getCategoryById(categoryData, studyStat.categoryId) + : studyDataUtils.getCategoryByTemporaryName(categoryData, studyStat.name!); + const { colorKey: categoryColor } = useCategoryColor(categoryInfo.id); + + return ( + + ); +}; + export default SelectedDateStat; \ No newline at end of file diff --git a/src/screens/group/GroupChatScreen/hooks/useGroupChatBootstrap.ts b/src/screens/group/GroupChatScreen/hooks/useGroupChatBootstrap.ts index ce95ffe7..9a193efd 100644 --- a/src/screens/group/GroupChatScreen/hooks/useGroupChatBootstrap.ts +++ b/src/screens/group/GroupChatScreen/hooks/useGroupChatBootstrap.ts @@ -51,7 +51,7 @@ export const useGroupChatBootstrap = ({ await loadMoreLocal(); const cursor = - (await asyncStorage.get(ASYNC_KEYS.groupChatCursor(chatRoomId))) ?? undefined; + (await asyncStorage.get(ASYNC_KEYS.groupChatCursor(myUserId, chatRoomId))) ?? undefined; const chatData = await queryClient.ensureQueryData( chatRoomInfoAfterCursorQueryOptions(chatRoomId, cursor), @@ -71,7 +71,7 @@ export const useGroupChatBootstrap = ({ const entities = fetchedChats.map(chat => chatObjToEntity(chat, chatRoomId)); await storeChats(entities); const recentChatId = fetchedChats[fetchedChats.length - 1].chatId; - asyncStorage.set(ASYNC_KEYS.groupChatCursor(chatRoomId), recentChatId); + asyncStorage.set(ASYNC_KEYS.groupChatCursor(myUserId, chatRoomId), recentChatId); sendReadCursor({ roomId: chatRoomId, type: 'READ', content: recentChatId }); } diff --git a/src/screens/group/GroupChatScreen/hooks/useGroupChatRealtime.ts b/src/screens/group/GroupChatScreen/hooks/useGroupChatRealtime.ts index a659d344..9b2a98fe 100644 --- a/src/screens/group/GroupChatScreen/hooks/useGroupChatRealtime.ts +++ b/src/screens/group/GroupChatScreen/hooks/useGroupChatRealtime.ts @@ -13,7 +13,7 @@ import { chatObjToEntity } from '../utils'; type Params = { isConnected: boolean; chatRoomId: string; - myUserId: number | null; + myUserId: number; memberMap: Map; tUnknown: string; @@ -55,7 +55,7 @@ export const useGroupChatRealtime = ({ }; await storeChats([chatObjToEntity(newChat, chatRoomId)]); - await asyncStorage.set(ASYNC_KEYS.groupChatCursor(chatRoomId), message.id); + await asyncStorage.set(ASYNC_KEYS.groupChatCursor(myUserId, chatRoomId), message.id); sendReadCursor({ roomId: chatRoomId, type: 'READ', content: message.id }); if (message.sender !== myUserId) { diff --git a/src/screens/group/GroupChatScreen/index.tsx b/src/screens/group/GroupChatScreen/index.tsx index 86005f6d..1428d006 100644 --- a/src/screens/group/GroupChatScreen/index.tsx +++ b/src/screens/group/GroupChatScreen/index.tsx @@ -23,7 +23,6 @@ import { useGroupMemberMap } from './hooks/useGroupMemberMap'; // TODO: 채팅방 진입 시 마지막으로 본 채팅으로 스크롤 이동 // TODO: 윈도우에서 채팅 시간이 YYYY.MM.DD 형식으로 나오는 문제 해결 // TODO: 채팅 읽음 커서 기능구현 -// eslint-disable-next-line max-lines-per-function const GroupChatScreen = () => { const tUnknown = useTranslatedText({ tKey: 'general.unknown' }); const { params } = useStackRoute<'GroupChat'>(); @@ -37,7 +36,7 @@ const GroupChatScreen = () => { const { loadMoreChats, storeChats } = useChatDB({ chatRoomId, - userId: profileData?.id ?? null, + userId: profileData?.id ?? -1, }); const [chats, setChats] = useState([]); @@ -50,14 +49,14 @@ const GroupChatScreen = () => { loadMoreChats, storeChats, setChats, - myUserId: profileData?.id ?? null, + myUserId: profileData?.id ?? -1, sendReadCursor, }); useGroupChatRealtime({ isConnected, chatRoomId, - myUserId: profileData?.id ?? null, + myUserId: profileData?.id ?? -1, memberMap, tUnknown, storeChats, @@ -90,6 +89,7 @@ const GroupChatScreen = () => { /> } keyboardControllerType='keyboardAvoidingView' + safeAreas='topAndBottom' title={groupName} > { return ( diff --git a/src/screens/group/GroupSettingScreen/index.tsx b/src/screens/group/GroupSettingScreen/index.tsx index b2afea7c..f29ae6d8 100644 --- a/src/screens/group/GroupSettingScreen/index.tsx +++ b/src/screens/group/GroupSettingScreen/index.tsx @@ -17,7 +17,7 @@ const GroupSettingScreen = () => { return ( diff --git a/src/screens/group/groupDetail/GroupDetailScreen/index.tsx b/src/screens/group/groupDetail/GroupDetailScreen/index.tsx index b869d773..8ccac740 100644 --- a/src/screens/group/groupDetail/GroupDetailScreen/index.tsx +++ b/src/screens/group/groupDetail/GroupDetailScreen/index.tsx @@ -1,4 +1,4 @@ -import { useFocusEffect } from '@react-navigation/native'; +import { useFocusEffect } from '@react-navigation/native'; import type { Day } from 'date-fns'; import { useCallback, useMemo, useState } from 'react'; @@ -7,19 +7,19 @@ import { GROUP_COLORS } from '@/constants/groupColors'; import { useCategoriesQuery } from '@/features/category/api/queries'; import { isDayInDayBelong } from '@/features/category/model/dayBelongHelper'; import { useGroupDetailQuery } from '@/features/groups/api/queries'; +import { useGroupColorKey } from '@/features/groups/hooks/useGroupColorKey'; import { useChatRoomListSse, useStackRoute } from '@/hooks'; import { StackableScreenLayout } from '@/layout'; import { studyDataUtils } from '@/store/studySlice'; -import { theme } from '@/theme'; import GroupInfoPanel from '../components/GroupInfoPanel'; import TodaysGoalPanel from '../components/TodaysGoalPanel'; const GroupDetailScreen = () => { - const groupColor = GROUP_COLORS[theme.color.blue[300]]; - const { data: categories } = useCategoriesQuery(); const { params } = useStackRoute<'GroupDetail'>(); + const { colorKey } = useGroupColorKey(params.groupId); + const groupColor = colorKey ? GROUP_COLORS[colorKey] : null; const { data, isPending } = useGroupDetailQuery(params.groupId); const [chatCount, setChatCount] = useState(null); @@ -54,11 +54,12 @@ const GroupDetailScreen = () => { // TODO: Fallback UI if (isPending || !data) return null; + if (!groupColor) return null; return ( StyleSheet.create({ + container: { + width: 40, + height: 40, + borderRadius: theme.radius['max'], + borderWidth: selected ? 3 : 0, + borderColor: GROUP_COLORS[colorKey].medium, + }, + colorDisplay: { + flex: 1, + margin: 4, + backgroundColor: GROUP_COLORS[colorKey].medium, + borderRadius: theme.radius['max'], + }, +}); \ No newline at end of file diff --git a/src/screens/group/groupDetail/components/GroupColorPickerDialog/ColorPickItem/index.tsx b/src/screens/group/groupDetail/components/GroupColorPickerDialog/ColorPickItem/index.tsx new file mode 100644 index 00000000..61f138c8 --- /dev/null +++ b/src/screens/group/groupDetail/components/GroupColorPickerDialog/ColorPickItem/index.tsx @@ -0,0 +1,30 @@ +import { Pressable, View } from 'react-native'; + +import type { GroupColorKey } from '@/constants/groupColors'; + +import { createStyles } from './index.style'; + +interface ColorPickItemProps { + colorKey: GroupColorKey; + isSelected: boolean; + onSelect: (colorKey: GroupColorKey) => void; +} + +const ColorPickItem = ({ + colorKey, + isSelected, + onSelect, +}: ColorPickItemProps) => { + const styles = createStyles(isSelected, colorKey); + + return ( + onSelect(colorKey)} + style={styles.container} + > + + + ); +}; + +export default ColorPickItem; diff --git a/src/screens/group/groupDetail/components/GroupColorPickerDialog/index.style.ts b/src/screens/group/groupDetail/components/GroupColorPickerDialog/index.style.ts new file mode 100644 index 00000000..073e7dd8 --- /dev/null +++ b/src/screens/group/groupDetail/components/GroupColorPickerDialog/index.style.ts @@ -0,0 +1,22 @@ +import { StyleSheet } from 'react-native'; + +import type { TextProps } from '@/components/Text'; +import type { GroupColorKey } from '@/constants/groupColors'; +import { GROUP_COLORS } from '@/constants/groupColors'; + +export const styles = StyleSheet.create({ + container: { + alignItems: 'center', + justifyContent: 'center', + gap: 24, + }, + pickerContainer: { + flexDirection: 'row', + gap: 12, + }, +}); + +export const PICKER_TITLE_TEXT_STYLE = (groupColorKey: GroupColorKey): TextProps => ({ + font: 't1', + color: GROUP_COLORS[groupColorKey].strong, +}); \ No newline at end of file diff --git a/src/screens/group/groupDetail/components/GroupColorPickerDialog/index.tsx b/src/screens/group/groupDetail/components/GroupColorPickerDialog/index.tsx new file mode 100644 index 00000000..dafa2fff --- /dev/null +++ b/src/screens/group/groupDetail/components/GroupColorPickerDialog/index.tsx @@ -0,0 +1,43 @@ +import { View } from 'react-native'; + +import { Dialog, TText } from '@/components'; +import { GROUP_COLORS, type GroupColorKey } from '@/constants/groupColors'; + +import ColorPickItem from './ColorPickItem'; +import { PICKER_TITLE_TEXT_STYLE, styles } from './index.style'; + +interface GroupColorPickerProps { + groupColorKey: GroupColorKey; + onSelectColor: (groupColorKey: GroupColorKey) => void; +} + +const GroupColorPickerDialog = ({ + groupColorKey, + onSelectColor, +}: GroupColorPickerProps) => ( + + {({ closeDialog }) => ( + + + + {Object.keys(GROUP_COLORS).map(key => ( + { + onSelectColor(selectedColorKey); + closeDialog(); + }} + /> + ))} + + + )} + +); + +export default GroupColorPickerDialog; \ No newline at end of file diff --git a/src/screens/group/groupDetail/components/GroupInfoPanel/DashBoardButtonGroup/index.tsx b/src/screens/group/groupDetail/components/GroupInfoPanel/DashBoardButtonGroup/index.tsx index 27a518f7..40dffe29 100644 --- a/src/screens/group/groupDetail/components/GroupInfoPanel/DashBoardButtonGroup/index.tsx +++ b/src/screens/group/groupDetail/components/GroupInfoPanel/DashBoardButtonGroup/index.tsx @@ -3,10 +3,14 @@ import { View } from 'react-native'; import { Icon, Toast } from '@/components'; import type { GroupColorKey } from '@/constants/groupColors'; import { GROUP_COLORS } from '@/constants/groupColors'; +import { useGroupColorKey } from '@/features/groups/hooks/useGroupColorKey'; import type { GroupMember } from '@/features/groups/model'; +import { useProfileQuery } from '@/features/user/api/queries'; import { useStackNavigation } from '@/hooks/useStackNavigation'; import { useTranslatedText } from '@/hooks/useTranslatedText'; +import { dialogService } from '@/utils/dialog'; +import GroupColorPickerDialog from '../../GroupColorPickerDialog'; import DashBoardButton from '../DashBoardButton'; import ChatButton from '../DashBoardButton/ChatButton'; import { createStyles } from './index.style'; @@ -22,41 +26,45 @@ interface DashBoardButtonGroupProps { disabled: boolean; } +// eslint-disable-next-line max-lines-per-function const DashBoardButtonGroup = ({ - groupName, - groupColorKey, - chatCount, - isHost, - chatRoomId, - members, - groupId, - disabled, + groupName, groupColorKey, chatCount, isHost, chatRoomId, members, groupId, disabled, }: DashBoardButtonGroupProps) => { const tRanking = useTranslatedText({ tKey: 'groupDetail.ranking' }); - const tStatistics = useTranslatedText({ tKey: 'groupDetail.statistics' }); + const tColor = useTranslatedText({ tKey: 'groupDetail.color' }); const tSetting = useTranslatedText({ tKey: 'groupDetail.setting' }); const tCanUseAfterJoining = useTranslatedText({ tKey: 'groupDetail.can-use-after-joining' }); const navigation = useStackNavigation(); const styles = createStyles(groupColorKey, disabled); const buttonPrimaryColor = GROUP_COLORS[groupColorKey].medium; + + const { updateColorKey } = useGroupColorKey(groupId); + const { data: profileData } = useProfileQuery(); + if (!profileData) return null; const handleRankingPress = () => { if (disabled) { Toast.show(tCanUseAfterJoining); return; } - navigation.navigate('GroupRanking', { - groupId, - groupColorKey, - }); + navigation.navigate('GroupRanking', { groupId, groupColorKey }); }; - const handleStatisticsPress = () => { + const handleColorPress = () => { if (disabled) { Toast.show(tCanUseAfterJoining); return; } + dialogService.add({ + content: ( + { + updateColorKey(selectedColorKey); + }} + />), + }); }; const handleChatPress = () => { @@ -64,12 +72,7 @@ const DashBoardButtonGroup = ({ Toast.show(tCanUseAfterJoining); return; } - navigation.navigate('GroupChat', { - chatRoomId, - groupName, - groupColorKey, - members, - }); + navigation.navigate('GroupChat', { chatRoomId, groupName, groupColorKey, members }); }; const handleSettingsPress = () => { @@ -77,10 +80,7 @@ const DashBoardButtonGroup = ({ Toast.show(tCanUseAfterJoining); return; } - navigation.navigate('GroupSetting', { - groupId, - groupColorKey, - }); + navigation.navigate('GroupSetting', { groupId, groupColorKey }); }; return ( @@ -92,19 +92,19 @@ const DashBoardButtonGroup = ({ onPress={handleRankingPress} title={tRanking} /> - + {isHost && `groupChat:${chatRoomId}`, + groupChatCursor: (userId: number, chatRoomId: string) => `groupChat:${userId}-${chatRoomId}`, + groupColor: (userId: number, groupId: number) => `groupColor:${userId}-${groupId}`, }; \ No newline at end of file