Skip to content
Open
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
200 changes: 122 additions & 78 deletions src/components/coffeechat/CoffeeChatCategory/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { colors } from '@sopt-makers/colors';
import { fonts } from '@sopt-makers/fonts';
import { IconChevronDown } from '@sopt-makers/icons';
import { SearchField, SelectV2 } from '@sopt-makers/ui';
import { useRouter } from 'next/router';
import { ParsedUrlQuery } from 'querystring';
import { useEffect, useState } from 'react';

import { useGetMembersCoffeeChat } from '@/api/endpoint/members/getMembersCoffeeChat';
Expand All @@ -19,6 +21,7 @@ import Loading from '@/components/common/Loading';
import Responsive from '@/components/common/Responsive';
import { LoggingClick } from '@/components/eventLogger/components/LoggingClick';
import useEventLogger from '@/components/eventLogger/hooks/useEventLogger';
import { usePageQueryParams } from '@/hooks/usePageQueryParams';
import {
MB_BIG_MEDIA_QUERY,
MB_MID_MEDIA_QUERY,
Expand All @@ -30,52 +33,90 @@ import {
} from '@/styles/mediaQuery';

export default function CoffeeChatCategory() {
const [section, setSection] = useState('');
const [topicType, setTopicType] = useState('');
const [career, setCareer] = useState('');
const [part, setPart] = useState('');
const [search, setSearch] = useState('');
const [clientSearch, setClientSearch] = useState('');
const [queryParams, setQueryParams] = useState({
...(section && section !== '전체' && { section: section === '프론트엔드' ? '프론트' : section }),
...(topicType && topicType !== '전체' && { topicType }),
...(career &&
career !== '전체' && {
career: career === '인턴' ? '인턴 경험만 있어요' : career === '아직 없음' ? '아직 없어요' : career,
}),
...(part && part !== '전체' && { part }),
...(search && { search }), // search는 빈 문자열이 아닌 경우만 추가}
});
const router = useRouter();
const { addQueryParamsToUrl } = usePageQueryParams({ skipNull: true });

const getQueryParamAsString = (param: string | string[] | undefined): string => {
if (Array.isArray(param)) return param[0] || ''; // 배열이면 첫 번째 값, 없으면 ''
return param ?? ''; // `undefined`이면 빈 문자열 반환
};

useEffect(() => {
setQueryParams({
...(section && section !== '전체' && { section: section === '프론트엔드' ? '프론트' : section }),
...(topicType && topicType !== '전체' && { topicType }),
...(career &&
career !== '전체' && {
career: career === '인턴' ? '인턴 경험만 있어요' : career === '아직 없음' ? '아직 없어요' : career,
}),
...(part && part !== '전체' && { part }),
...(search && { search }), // search는 빈 문자열이 아닌 경우만 추가}
});
}, [section, topicType, career, part, search]);
if (router.isReady) {
setSelectedSection(getQueryParamAsString(router.query.section));
setSelectedTopicType(getQueryParamAsString(router.query.topicType));
setSelectedCareer(getQueryParamAsString(router.query.career));
setSelectedPart(getQueryParamAsString(router.query.part));
setClientSearch(getQueryParamAsString(router.query.search));
}
}, [router.isReady, router.query]);

const search = getQueryParamAsString(router.query.search);

const [selectedSection, setSelectedSection] = useState('');
const [selectedTopicType, setSelectedTopicType] = useState('');
const [selectedCareer, setSelectedCareer] = useState('');
const [selectedPart, setSelectedPart] = useState('');
const [clientSearch, setClientSearch] = useState(search);

const handleFilterChange = (filterType: string, value: string) => {
addQueryParamsToUrl({ [filterType]: value });
};
const formatSoptActivities = (soptActivities: string[]) => {
const generations = soptActivities
.map((item) => parseInt(item.match(/^\d+/)?.[0] || '', 10)) // 숫자 문자열을 숫자로 변환
.filter((num) => !isNaN(num)); // NaN 값 제거
const parts = [...new Set(soptActivities.map((item) => item.replace(/^\d+기 /, '')))];
return { generation: generations, part: parts };
};
const { data, isLoading } = useGetMembersCoffeeChat(queryParams);

const formatQueryParams = (query: ParsedUrlQuery): { [key: string]: string } => {
Copy link
Member

Choose a reason for hiding this comment

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

현재 코드에서 normalizeCareer, normalizeSection...===('전체') return undefined; 등 값을 변환하는 로직이 반복적으로 적용이 되고 있어서 해당 로직을 합쳐서 처리한다면 중복 코드가 제거되고 가독성이 올라갈 수 있을 것 같아요!

const normalizeCareer = (career: string | undefined): string | undefined => {
if (!career || career === '전체') return undefined;
return career === '인턴' ? '인턴 경험만 있어요' : career === '아직 없음' ? '아직 없어요' : career;
Copy link
Member

@seong-hui seong-hui Mar 19, 2025

Choose a reason for hiding this comment

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

해당 부분을 상수화해서 아래와 같은 객체로 정의하여 CAREER_MAPPING['인턴']처럼 조회를 할 수 있게하면 좋을 것 같아요!

 const CAREER_MAPPING: Record<string, string> = {
    '인턴': '인턴 경험만 있어요',
    '아직 없음': '아직 없어요',
  };

  const SECTION_MAPPING: Record<string, string> = {
    '프론트엔드': '프론트',
  };

};

const normalizeSection = (section: string | undefined): string | undefined => {
if (!section || section === '전체') return undefined;
return section === '프론트엔드' ? '프론트' : section;
};

return Object.fromEntries(
Object.entries(query)
.map(([key, value]) => {
const strValue = Array.isArray(value) ? value[0] : value;

if (typeof strValue !== 'string') return [key, undefined];

switch (key) {
Copy link
Member

Choose a reason for hiding this comment

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

현재는 각각 다르게 normalize를 진행하기 위해서 case를 나누었는데 모든 case에 적용할 수 있는 normalize로직으로 수정한다면 해당 부분도 아래와 같이 표현할 수 있을 것 같네요!

return Object.fromEntries(
      Object.entries(query)
        .map(([key, value]) => {
          const strValue = Array.isArray(value) ? value[0] : value;

          if (typeof strValue !== 'string') return [key, undefined];

          return [key, normalizeValue(key, strValue)];
        })
        .filter(([_, value]) => value !== undefined), // undefined 값 제거
    );

case 'career':
return [key, normalizeCareer(strValue)];
case 'section':
return [key, normalizeSection(strValue)];
case 'topicType':
case 'part':
return [key, strValue !== '전체' ? strValue : undefined];
case 'search':
return [key, strValue || undefined];
default:
return [key, strValue];
}
})
.filter(([_, value]) => value !== undefined), // undefined 값 제거
);
};

const { data, isLoading } = useGetMembersCoffeeChat(formatQueryParams(router.query));

const { logSubmitEvent } = useEventLogger();
const SelectionArea = (): JSX.Element => {
return (
<>
<SelectV2.Root
className='topic-select'
onChange={(e: number) => setTopicType(TOPIC_FILTER_OPTIONS[e - 1].label)}
onChange={(e: number) => handleFilterChange('topicType', TOPIC_FILTER_OPTIONS[e - 1].label)}
type='text'
defaultValue={TOPIC_FILTER_OPTIONS.find((option) => option.label === topicType)}
defaultValue={TOPIC_FILTER_OPTIONS.find((option) => option.label === selectedTopicType)}
visibleOptions={4}
>
<SelectV2.Trigger>
Expand All @@ -86,9 +127,9 @@ export default function CoffeeChatCategory() {
<LoggingClick
eventKey='coffeechatFilter'
param={{
topic_tag: topicType,
career: career,
part: part,
topic_tag: selectedTopicType,
career: selectedCareer,
part: selectedPart,
}}
key={option.label}
>
Expand All @@ -100,8 +141,8 @@ export default function CoffeeChatCategory() {

<SelectV2.Root
className='career-select'
onChange={(e: number) => setCareer(CAREER_FILTER_OPTIONS[e - 1].label)}
defaultValue={CAREER_FILTER_OPTIONS.find((option) => option.label === career)}
onChange={(e: number) => handleFilterChange('career', CAREER_FILTER_OPTIONS[e - 1].label)}
defaultValue={CAREER_FILTER_OPTIONS.find((option) => option.label === selectedCareer)}
type='text'
visibleOptions={4}
>
Expand All @@ -113,9 +154,9 @@ export default function CoffeeChatCategory() {
<LoggingClick
eventKey='coffeechatFilter'
param={{
topic_tag: topicType,
career: career,
part: part,
topic_tag: selectedTopicType,
career: selectedCareer,
part: selectedPart,
}}
key={option.label}
>
Expand All @@ -127,8 +168,8 @@ export default function CoffeeChatCategory() {

<SelectV2.Root
className='part-select'
onChange={(e: number) => setPart(PART_FILTER_OPTIONS[e - 1].label)}
defaultValue={PART_FILTER_OPTIONS.find((option) => option.label === part)}
onChange={(e: number) => handleFilterChange('part', PART_FILTER_OPTIONS[e - 1].label)}
defaultValue={PART_FILTER_OPTIONS.find((option) => option.label === selectedPart)}
type='text'
visibleOptions={4}
>
Expand All @@ -140,9 +181,9 @@ export default function CoffeeChatCategory() {
<LoggingClick
eventKey='coffeechatFilter'
param={{
topic_tag: topicType,
career: career,
part: part,
topic_tag: selectedTopicType,
career: selectedCareer,
part: selectedPart,
}}
key={option.label}
>
Expand All @@ -164,8 +205,8 @@ export default function CoffeeChatCategory() {
{categoryList.categoryList.map((option) => (
<LoggingClick eventKey='coffeechatSection' key={option.categoryName} param={{ section: option.categoryName }}>
<CategoryCard
isActive={section === option.categoryName}
onClick={() => setSection(option.categoryName)}
isActive={selectedSection === option.categoryName}
onClick={() => handleFilterChange('section', option.categoryName)}
key={option.categoryName}
>
<CardIcon src={option.icon}></CardIcon>
Expand All @@ -185,11 +226,11 @@ export default function CoffeeChatCategory() {
onChange={(e) => setClientSearch(e.target.value)}
onSubmit={() => {
logSubmitEvent('searchCoffeeChat', {
search_content: clientSearch,
search_content: search,
});
setSearch(clientSearch);
addQueryParamsToUrl({ search: clientSearch || undefined });
}}
onReset={() => setClientSearch('')}
onReset={() => handleFilterChange('search', '')}
/>
Comment on lines +229 to 234
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

검색 초기화 시 search 쿼리 파라미터가 남습니다

handleFilterChange('search', '')로 빈 문자열을 전달하면 skipNull: true 옵션에도 불구하고 '' 값이 그대로 URL에 유지됩니다.
또한 로그에 실제 입력값이 아닌 기존 search 값을 사용하고 있어 분석 데이터가 부정확할 수 있습니다.

-  logSubmitEvent('searchCoffeeChat', {
-    search_content: search,
-  });
-  addQueryParamsToUrl({ search: clientSearch || undefined });
+  logSubmitEvent('searchCoffeeChat', {
+    search_content: clientSearch,
+  });
+  addQueryParamsToUrl({ search: clientSearch || undefined });
...
- onReset={() => handleFilterChange('search', '')}
+ onReset={() => handleFilterChange('search', undefined)}

이렇게 수정하면

  1. 실제 사용자가 입력한 값을 로그에 남기고
  2. 검색어를 비울 때 쿼리 파라미터가 완전히 제거됩니다.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
search_content: search,
});
setSearch(clientSearch);
addQueryParamsToUrl({ search: clientSearch || undefined });
}}
onReset={() => setClientSearch('')}
onReset={() => handleFilterChange('search', '')}
/>
logSubmitEvent('searchCoffeeChat', {
search_content: clientSearch,
});
addQueryParamsToUrl({ search: clientSearch || undefined });
}}
onReset={() => handleFilterChange('search', undefined)}
/>
🤖 Prompt for AI Agents
In src/components/coffeechat/CoffeeChatCategory/index.tsx around lines 229 to
234, the onReset handler calls handleFilterChange with an empty string, but this
leaves the 'search' query parameter in the URL and logs the old search value
instead of the actual input. To fix this, update the onReset function to pass
undefined or null instead of an empty string to handleFilterChange so that the
'search' parameter is removed from the URL, and ensure the logging uses the
current input value to accurately reflect user actions.

</FilterArea>
</Responsive>
Expand All @@ -200,49 +241,49 @@ export default function CoffeeChatCategory() {
onChange={(e) => setClientSearch(e.target.value)}
onSubmit={() => {
logSubmitEvent('searchCoffeeChat', {
search_content: clientSearch,
search_content: search,
});
setSearch(clientSearch);
addQueryParamsToUrl({ search: clientSearch || undefined });
}}
onReset={() => setClientSearch('')}
onReset={() => handleFilterChange('seasrch', '')}
Comment on lines 242 to +248
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

onReset 키 오타로 모바일에서 검색 초기화 실패

'seasrch''search' 오타 때문에 모바일 필터에서 검색어가 제대로 초기화되지 않습니다.

- onReset={() => handleFilterChange('seasrch', '')}
+ onReset={() => handleFilterChange('search', undefined)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onSubmit={() => {
logSubmitEvent('searchCoffeeChat', {
search_content: clientSearch,
search_content: search,
});
setSearch(clientSearch);
addQueryParamsToUrl({ search: clientSearch || undefined });
}}
onReset={() => setClientSearch('')}
onReset={() => handleFilterChange('seasrch', '')}
onSubmit={() => {
logSubmitEvent('searchCoffeeChat', {
search_content: search,
});
addQueryParamsToUrl({ search: clientSearch || undefined });
}}
onReset={() => handleFilterChange('search', undefined)}
🤖 Prompt for AI Agents
In src/components/coffeechat/CoffeeChatCategory/index.tsx around lines 242 to
248, there is a typo in the onReset handler where 'seasrch' is used instead of
'search'. Correct the key from 'seasrch' to 'search' in the handleFilterChange
call to ensure the search filter resets properly on mobile devices.

/>
<StyledMobileFilterWrapper>
<StyledMobileFilter
value={section}
value={selectedSection}
onChange={(e: string) => {
setSection(SECTION_FILTER_OPTIONS[parseInt(e) - 1].label);
handleFilterChange('section', SECTION_FILTER_OPTIONS[parseInt(e) - 1].label);
}}
options={SECTION_FILTER_OPTIONS.map((option) => ({
value: option.value.toString(),
label: option.label,
}))}
placeholder='분야'
trigger={(placeholder) => (
<MobileFilterTrigger selected={section.length > 0} value={section}>
{section ? section : placeholder}
<MobileFilterTrigger selected={selectedSection.length > 0} value={selectedSection}>
{selectedSection ? selectedSection : placeholder}
<StyledChevronDown />
</MobileFilterTrigger>
)}
/>
<LoggingClick
eventKey='coffeechatFilter'
param={{
topic_tag: topicType,
career: career,
part: part,
topic_tag: selectedTopicType,
career: selectedCareer,
part: selectedPart,
}}
>
<StyledMobileFilter
value={topicType}
onChange={(e: string) => setTopicType(TOPIC_FILTER_OPTIONS[parseInt(e) - 1].label)}
value={selectedTopicType}
onChange={(e: string) => handleFilterChange('topicType', TOPIC_FILTER_OPTIONS[parseInt(e) - 1].label)}
options={TOPIC_FILTER_OPTIONS.map((option) => ({
value: option.value.toString(),
label: option.label,
}))}
placeholder='주제'
trigger={(placeholder) => (
<MobileFilterTrigger selected={topicType.length > 0}>
{topicType ? topicType : placeholder}
<MobileFilterTrigger selected={selectedTopicType.length > 0}>
{selectedTopicType ? selectedTopicType : placeholder}
<StyledChevronDown />
</MobileFilterTrigger>
)}
Expand All @@ -251,22 +292,22 @@ export default function CoffeeChatCategory() {
<LoggingClick
eventKey='coffeechatFilter'
param={{
topic_tag: topicType,
career: career,
part: part,
topic_tag: selectedTopicType,
career: selectedCareer,
part: selectedPart,
}}
>
<StyledMobileFilter
value={career}
onChange={(e: string) => setCareer(CAREER_FILTER_OPTIONS[parseInt(e) - 1].label)}
value={selectedCareer}
onChange={(e: string) => handleFilterChange('career', CAREER_FILTER_OPTIONS[parseInt(e) - 1].label)}
options={CAREER_FILTER_OPTIONS.map((option) => ({
value: option.value.toString(),
label: option.label,
}))}
placeholder='경력'
trigger={(placeholder) => (
<MobileFilterTrigger selected={career.length > 0}>
{career ? career : placeholder}
<MobileFilterTrigger selected={selectedCareer.length > 0}>
{selectedCareer ? selectedCareer : placeholder}
<StyledChevronDown />
</MobileFilterTrigger>
)}
Expand All @@ -275,22 +316,22 @@ export default function CoffeeChatCategory() {
<LoggingClick
eventKey='coffeechatFilter'
param={{
topic_tag: topicType,
career: career,
part: part,
topic_tag: selectedTopicType,
career: selectedCareer,
part: selectedPart,
}}
>
<StyledMobileFilter
value={part}
onChange={(e: string) => setPart(PART_FILTER_OPTIONS[parseInt(e) - 1].label)}
value={selectedPart}
onChange={(e: string) => handleFilterChange('part', PART_FILTER_OPTIONS[parseInt(e) - 1].label)}
options={PART_FILTER_OPTIONS.map((option) => ({
value: option.value.toString(),
label: option.label,
}))}
placeholder='파트'
trigger={(placeholder) => (
<MobileFilterTrigger selected={part.length > 0}>
{part ? part : placeholder}
<MobileFilterTrigger selected={selectedPart.length > 0}>
{selectedPart ? selectedPart : placeholder}
<StyledChevronDown />
</MobileFilterTrigger>
)}
Expand Down Expand Up @@ -321,9 +362,12 @@ export default function CoffeeChatCategory() {
career: item.career === '아직 없음' ? '없음' : item.career?.split(' ')[0],
organization: item?.organization,
job: item.companyJob || undefined,
section: section,
section: selectedSection,
title: item.bio || undefined,
topic_tag: topicType && topicType !== '' && topicType !== '전체' ? topicType : undefined,
topic_tag:
selectedTopicType && selectedTopicType !== '' && selectedTopicType !== '전체'
? selectedTopicType
: undefined,
...formatSoptActivities(item?.soptActivities || []),
}}
>
Expand Down
Loading