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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ yarn-error.log
# build cache & logs
msbuild.binlog
android/.kotlin/*
tsconfig.tsbuildinfo

# env
.env
Expand Down
4 changes: 2 additions & 2 deletions src/components/Button/Icon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { Icon, IconProps } from '@/types/icon';
interface IconButtonProps extends Omit<IconProps, 'style'> {
Icon: Icon;
buttonPadding?: number;
style?: StyleProp<ViewStyle>;
containerStyle?: StyleProp<ViewStyle>;
iconStyle?: StyleProp<ViewStyle>;
buttonType?: 'touchableOpacity' | 'pressable';
onPress?: () => void;
Expand All @@ -17,7 +17,7 @@ const IconButton = ({
Icon,
onPress,
buttonPadding = 2,
style: containerStyle,
containerStyle,
iconStyle,
buttonType = 'touchableOpacity',
...props
Expand Down
2 changes: 1 addition & 1 deletion src/components/Panel/PanelView/PanelHeader.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const TITLE_TEXT_STYLE: TextProps = {
export const BUTTON_ICON_STYLE: IconProps = {
width: 20,
height: 20,
style: {
containerStyle: {
position: 'absolute',
right: 0, top: 0,
},
Expand Down
5 changes: 3 additions & 2 deletions src/features/category/hooks/useCategoryColor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import { type GroupColorKey } from '@/constants/groupColors';
import { useGroupsQuery } from '@/features/groups/api/queries';
import { useGroupColorKey } from '@/features/groups/hooks/useGroupColorKey';
import { useGroupColorKeyMap } from '@/features/groups/hooks/useGroupColorKey';
import { theme } from '@/theme';

const DEFAULT_COLOR_KEY: GroupColorKey = theme.color.primary[500];
Expand All @@ -12,7 +12,8 @@ export const useCategoryColor = (categoryId: number) => {
const matchingGroupId =
groupsData?.find((g) => g.categoryIds.includes(categoryId))?.groupId ?? null;

const { colorKey: groupColor } = useGroupColorKey(matchingGroupId);
const { colorKeyMap } = useGroupColorKeyMap();
const groupColor = matchingGroupId ? colorKeyMap.get(matchingGroupId) : null;
const colorKey = groupColor ?? DEFAULT_COLOR_KEY;

return { colorKey };
Expand Down
2 changes: 1 addition & 1 deletion src/features/category/ui/InlineCategoryItem/index.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing[100],
gap: theme.spacing[200],
},
});

Expand Down
72 changes: 46 additions & 26 deletions src/features/groups/hooks/useGroupColorKey.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,70 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useQueries, useQueryClient } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';

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';

import { useGroupsQuery } from '../api/queries';

const GROUP_COLOR_KEYS = Object.keys(GROUP_COLORS) as GroupColorKey[];
const queryKey = (userId: number | null, groupId: number) => ['groupColor', userId, groupId];

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;
export const useGroupColorKeyMap = () => {
const groups = useGroupsQuery().data;
const groupIds = useMemo(
() => Array.from(new Set(groups?.map(g => g.groupId))).sort((a, b) => a - b),
[groups],
);
const userId = useProfileQuery().data?.id ?? null;
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 results = useQueries({
queries: groupIds.map((groupId) => ({
queryKey: queryKey(userId, groupId),
enabled: userId != null && groupId != null,
queryFn: async (): Promise<GroupColorKey | null> => {
if (userId == null) return null;
const storageKey = ASYNC_KEYS.groupColor(userId, groupId);
const stored = await asyncStorage.get(storageKey);

const storageKey = ASYNC_KEYS.groupColor(userId, groupId);
const stored = await asyncStorage.get(storageKey);
if (stored && isGroupColorKey(stored)) return stored;

if (stored && isGroupColorKey(stored)) return stored;
const newKey = pickRandomGroupColorKey();
await asyncStorage.set(storageKey, newKey);
return newKey;
},
staleTime: Infinity,
gcTime: Infinity,
})),
});

const newKey = pickRandomGroupColorKey();
await asyncStorage.set(storageKey, newKey);
return newKey;
},
staleTime: Infinity,
gcTime: Infinity,
const colorKeyMap = new Map<number, GroupColorKey>();
groupIds.forEach((groupId, idx) => {
const colorKey = results[idx]?.data ?? null;
if (colorKey) colorKeyMap.set(groupId, colorKey);
});

const updateColorKey = async (next: GroupColorKey) => {
if (userId == null || groupId == null) return;
const updateColorKey = useCallback(
async (groupId: number, next: GroupColorKey) => {
if (userId == null) return;
qc.setQueryData(queryKey(userId, groupId), next);
await asyncStorage.set(ASYNC_KEYS.groupColor(userId, groupId), next);
},
[qc, userId],
);

qc.setQueryData(queryKey, next);
await asyncStorage.set(ASYNC_KEYS.groupColor(userId, groupId), next);
return {
colorKeyMap: colorKeyMap,
updateColorKey,
isReady: userId != null,
};

return { colorKey: colorKey ?? null, updateColorKey };
};

33 changes: 33 additions & 0 deletions src/hooks/useCategoryData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { GroupColorKey } from '@/constants/groupColors';
import { useCategoriesQuery } from '@/features/category/api/queries';
import type { GetCategoriesResponse } from '@/features/category/model';
import { useGroupsQuery } from '@/features/groups/api/queries';
import { useGroupColorKeyMap } from '@/features/groups/hooks/useGroupColorKey';
import { useStudyStatByDateQuery } from '@/features/study/api/queries';
import { studyDataUtils } from '@/store/studySlice';

Expand Down Expand Up @@ -39,4 +43,33 @@ export const useTodayGoals = () => {
export const useTodayStudyRecords = () => {
const { data: todayStudyRecords = [] } = useStudyStatByDateQuery(new Date());
return todayStudyRecords;
};

export const useSplitCategoriesByGroup = (
categories: GetCategoriesResponse['data'],
) => {
const { data: groups = [] } = useGroupsQuery();
const { colorKeyMap } = useGroupColorKeyMap();

return categories.reduce(
(acc, category) => {
const belongingGroup = groups.find(group =>
group.categoryIds.includes(category.id),
);
if (belongingGroup) {
const groupColorKey = colorKeyMap.get(belongingGroup.groupId);
if (!groupColorKey) return acc;
acc.groupCategories.push({ ...category, groupColorKey: groupColorKey });
Comment on lines 59 to 62

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

if (belongingGroup) 블록 내에 있어 belongingGroup이 항상 참이므로 삼항 연산자는 불필요합니다. 코드를 단순화할 수 있습니다.

그러나 더 중요한 문제가 있습니다. groupColorKey를 찾지 못하면(예: 데이터 로딩 중) 해당 카테고리가 '내 목표'와 '그룹 목표' 목록 양쪽에서 모두 누락되어 UI에서 일시적으로 사라지게 됩니다. 이는 사용자 경험에 좋지 않은 영향을 줍니다. useGroupColorKeyMapisReady 플래그를 사용하여 색상 데이터가 준비되었을 때만 이 로직을 실행하도록 하여 이 문제를 해결하는 것을 고려해 보세요.

        const groupColorKey = colorKeyMap.get(belongingGroup.groupId);
        if (!groupColorKey) return acc;
        acc.groupCategories.push({ ...category, groupColorKey });

} else {
acc.myCategories.push(category);
}
return acc;
},
{
groupCategories: [] as (
GetCategoriesResponse['data'][number] & { groupColorKey: GroupColorKey }
)[],
myCategories: [] as GetCategoriesResponse['data'],
},
);
};
3 changes: 2 additions & 1 deletion src/locales/ko/manageCategory.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"manage-my-goals": "내 목표 관리",
"my-goals": "나의 목표",
"my-goals": "내가 만든 목표",
"group-goals": "그룹에 속한 목표",
"goal-count": "총 {{count}}개",
"goal-time": "목표 시간 {{time}}",
"goal-time-null": "목표 시간 없음",
Expand Down
4 changes: 2 additions & 2 deletions src/navigation/BottomTabNavigator/BottomNavHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ const BottomNavHeader = () => {
<Button.Icon
Icon={Icon.Menu}
color={theme.color.primary[500]}
style={navigatorStyles.menuButton}
containerStyle={navigatorStyles.menuButton}
/>
<View style={navigatorStyles.headerRightContainer}>
<Button.Icon
Icon={Icon.Noti}
color={theme.color.primary[500]}
style={navigatorStyles.notiButton}
containerStyle={navigatorStyles.notiButton}
/>
<Pressable
onPress={() => navigation.navigate('Settings')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 { useGroupColorKeyMap } from '@/features/groups/hooks/useGroupColorKey';
import type { GetGroupsResponse } from '@/features/groups/model/groups';
import { useStackNavigation } from '@/hooks';
import { useTranslatedText } from '@/hooks/useTranslatedText';
Expand Down Expand Up @@ -38,8 +38,9 @@ const JoinedGroupPanel = () => {
};

const GroupListItem = ({ group }: { group: GetGroupsResponse['data'][number] }) => {
const { colorKey: groupColorKey } = useGroupColorKey(group.groupId);
if (!groupColorKey) return null;
const { colorKeyMap } = useGroupColorKeyMap();
const groupColorKey = colorKeyMap.get(group.groupId);
if (groupColorKey == null) return null;

const leaderImageUrl =
group.profiles.find(p => p.role === 'LEADER')?.imageUrl ?? null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,18 @@ export const styles = StyleSheet.create({
content: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing[100],
gap: theme.spacing[200],
},
groupColorIndicator: {
backgroundColor: theme.color.primary[500],
width: 10, height: 10,
borderRadius: theme.radius['max'],
},
iconContainer: {
borderWidth: 1,
borderRadius: 8,
paddingVertical: 4,
paddingHorizontal: 6,
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Button, Icon, Shapes, Text, TText } from '@/components';
import { useDeleteCategoryMutation } from '@/features/category/api/mutations';
import type { CategoryTag } from '@/features/category/model';
import { useStackNavigation } from '@/hooks';
import { theme } from '@/theme';
import { dialogService } from '@/utils/dialog';
import type { Time } from '@/utils/Time';

Expand Down Expand Up @@ -56,6 +57,7 @@ const CategoryItem = ({ id, title, goal, tags = [] }: CategoryItemProps) => {
return (
<View style={styles.container}>
<View style={styles.content}>
<View style={styles.groupColorIndicator} />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

groupColorIndicator 뷰는 '내가 만든 목표'를 표시하는 CategoryItem에 잘못 추가된 것으로 보입니다. '내가 만든 목표'는 특정 그룹에 속하지 않으므로 그룹 색상 표시가 필요 없습니다. 이 뷰와 관련 스타일(CategoryItem/index.style.tsgroupColorIndicator)을 제거하는 것이 좋겠습니다.

<Text {...GOAL_NAME_TEXT_STYLE}>
{title}
</Text>
Expand Down Expand Up @@ -86,12 +88,16 @@ const CategoryItem = ({ id, title, goal, tags = [] }: CategoryItemProps) => {
<View style={styles.content}>
<Button.Icon
Icon={Icon.Edit}
color={theme.color.blue[300]}
containerStyle={[styles.iconContainer, { borderColor: theme.color.blue[300] }]}
height={16}
onPress={onEditPress}
width={16}
/>
<Button.Icon
Icon={Icon.TrashBin}
color={theme.color.red[300]}
containerStyle={[styles.iconContainer, { borderColor: theme.color.red[300] }]}
height={16}
onPress={onDeletePress}
width={16}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { ViewStyle } from 'react-native';
import { StyleSheet } from 'react-native';

import type { TextProps } from '@/components/Text';
import { GROUP_COLORS, type GroupColorKey } from '@/constants/groupColors';
import { theme } from '@/theme';
import type { Color } from '@/types';

export const createStyles = ({
groupColorKey,
}: {
groupColorKey: GroupColorKey;
}) => StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: theme.spacing[200],
},
content: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing[200],
},
groupColorIndicator: {
backgroundColor: GROUP_COLORS[groupColorKey].medium,
width: 10, height: 10,
borderRadius: theme.radius['max'],
},
});

export const GOAL_NAME_TEXT_STYLE: TextProps = {
font: 'b3',
};

export const GOAL_TIME_TEXT_STYLE: TextProps = {
font: 'b4',
color: theme.color.neutral[500],
};

export const TAG_CONTAINER_STYLE = (color: Color): ViewStyle => ({
borderRadius: theme.radius['max'],
borderWidth: 1,
borderColor: color,
paddingVertical: theme.spacing[50],
paddingHorizontal: theme.spacing[100],
});

export const TAG_TEXT_STYLE = (color: Color): TextProps => ({
font: 'b5',
color,
});
Loading