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