Skip to content
8 changes: 8 additions & 0 deletions src/entities/host/api/hostChannelInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { axiosClient } from '../../../shared/types/api/http-client';
import { HostChannelInfoRequest, HostChannelInfoResponse } from '../model/hostChannelInfo';

const hostChannelInfo = async (dto: HostChannelInfoRequest) => {
const response = await axiosClient.get<HostChannelInfoResponse>(`/host-channels/${dto.hostChannelId}/info`);
return response.data;
};
export default hostChannelInfo;
12 changes: 12 additions & 0 deletions src/entities/host/hook/useHostChannelInfoHook.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query';
import hostChannelInfo from '../api/hostChannelInfo';

const useHostChannelInfo = (hostChannelId: number) => {
const { data } = useQuery({
queryKey: ['hostInfo', hostChannelId],
queryFn: () => hostChannelInfo({ hostChannelId }),
enabled: !!hostChannelId,
});
return { data };
};
export default useHostChannelInfo;
1 change: 1 addition & 0 deletions src/entities/host/hook/useHostDetailHook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const useHostDetail = (hostChannelId: number) => {
const { data } = useQuery({
queryKey: ['hostDetail', hostChannelId],
queryFn: () => hostDetail({ hostChannelId }),
enabled: !!hostChannelId,
});

return { data };
Expand Down
14 changes: 14 additions & 0 deletions src/entities/host/model/hostChannelInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface HostChannelInfoRequest {
hostChannelId: number;
}

export interface HostChannelInfoResponse {
result: {
id: number;
profileImageUrl: string;
hostChannelName: string;
channelDescription: string;
email: string;
hostChannelMembers: { id: number; memberName: string }[];
};
}
8 changes: 8 additions & 0 deletions src/features/host/api/host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { axiosClient } from '../../../shared/types/api/http-client';
import { UpdateHostChannelInfoRequest } from '../model/host';

const updateHostInfo = async (hostChannelId: number, dto: UpdateHostChannelInfoRequest) => {
const response = await axiosClient.put(`/host-channels/${hostChannelId}`, dto);
return response.data;
};
export default updateHostInfo;
14 changes: 14 additions & 0 deletions src/features/host/hook/useHostHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useMutation } from '@tanstack/react-query';
import updateHostInfo from '../api/host';
import { UpdateHostChannelInfoRequest } from '../model/host';
import { useParams } from 'react-router-dom';

export const useUpdateHostChannelInfo = () => {
const { id } = useParams();
const hostChannelId = Number(id);

const mutation = useMutation({
mutationFn: (dto: UpdateHostChannelInfoRequest) => updateHostInfo(hostChannelId, dto),
});
return mutation;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

뮤테이션 훅 구현이 잘 되어 있습니다.

호스트 채널 정보 업데이트를 위한 커스텀 훅이 적절하게 구현되어 있습니다. URL 파라미터에서 ID를 추출하고 뮤테이션을 설정하는 방식이 깔끔합니다.

다만, 몇 가지 개선 사항이 있습니다:

  1. ID 변환에 대한 유효성 검사가 필요합니다. (NaN 체크)
  2. 캐시 무효화 로직이 누락되어 있습니다.
  3. 성공/실패 처리가 설정되어 있지 않습니다.
export const useUpdateHostChannelInfo = () => {
  const { id } = useParams();
- const hostChannelId = Number(id);
+ const hostChannelId = id ? Number(id) : undefined;
+ 
+ if (!hostChannelId || isNaN(hostChannelId)) {
+   throw new Error('유효하지 않은 호스트 채널 ID입니다.');
+ }

  const mutation = useMutation({
    mutationFn: (dto: UpdateHostChannelInfoRequest) => updateHostInfo(hostChannelId, dto),
+   onSuccess: () => {
+     // React Query 클라이언트 import 필요
+     // queryClient.invalidateQueries(['hostChannelInfo', hostChannelId]);
+   },
  });
  return mutation;
};

Committable suggestion skipped: line range outside the PR's diff.

6 changes: 6 additions & 0 deletions src/features/host/model/host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface UpdateHostChannelInfoRequest {
profileImageUrl: string;
hostChannelName: string;
hostEmail: string;
channelDescription: string;
}
22 changes: 11 additions & 11 deletions src/pages/menu/ui/myHost/HostDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { useNavigate, useParams } from 'react-router-dom';
import HostDetailLayout from '../../../../shared/ui/backgrounds/HostDetailLayout';
import { trendingEvents } from '../../../../shared/types/eventCardType';
import EventCard from '../../../../shared/ui/EventCard';
import useHostDetail from '../../../../entities/host/hook/useHostDetailHook';

const HostDetailPage = () => {
const navigate = useNavigate();
// URL에서 hostId를 가져오기
const { id } = useParams<{ id: string }>();

// hostId에 해당하는 이벤트들만 필터링
const filteredEvents = trendingEvents.filter(event => event.id === Number(id));
const hostChannelId = Number(id);
const { data } = useHostDetail(hostChannelId);

Comment on lines +11 to 13
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

URL 파라미터 검증 로직 부재
Number(id) 결과가 NaN이면 useHostDetail 훅이 잘못된 파라미터로 호출됩니다. HostInfoPage와 동일한 방식으로 방어 코드를 추가해 주세요.

return (
<HostDetailLayout
Expand All @@ -20,16 +20,16 @@ const HostDetailPage = () => {
}
>
<div className="grid grid-cols-2 gap-4 mx-5 mt-3 md:grid-cols-2 lg:grid-cols-2 z-50">
{filteredEvents.map((event, index) => (
{data?.result.events.map(event => (
<EventCard
id={event.id}
key={index}
img={event.img}
eventTitle={event.eventTitle}
dDay={event.dDay}
host={event.host}
eventDate={event.eventDate}
location={event.location}
key={event.id}
img={event.bannerImageUrl}
eventTitle={event.title}
dDay={event.remainDays}
host={event.hostChannelName}
eventDate={event.startDate}
location={event.onlineType}
hashtags={event.hashtags}
/>
))}
Expand Down
58 changes: 48 additions & 10 deletions src/pages/menu/ui/myHost/HostEditPage.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import HostDetailLayout from '../../../../shared/ui/backgrounds/HostDetailLayout';
import ProfileCircle from '../../../../../design-system/ui/Profile';
import MultilineTextField from '../../../../../design-system/ui/textFields/MultilineTextField';
import DefaultTextField from '../../../../../design-system/ui/textFields/DefaultTextField';
import TertiaryButton from '../../../../../design-system/ui/buttons/TertiaryButton';
import { useParams } from 'react-router-dom';
import MemberEmailInput from '../../../../features/menu/ui/MemberEmailInput';
import { hostInfo } from '../../../../shared/types/hostInfoType';
import useHostChannelInfo from '../../../../entities/host/hook/useHostChannelInfoHook';
import { useUpdateHostChannelInfo } from '../../../../features/host/hook/useHostHook';
import { useQueryClient } from '@tanstack/react-query';

const HostEditPage = () => {
const { id } = useParams<{ id: string }>();
const [selectedHost, setSelectedHost] = useState(true);
const [selectedInfo, setSelectedInfo] = useState(false);
const [tags, setTags] = useState<string[]>([]);
const [inputValue, setInputValue] = useState('');
const [channelDescription, setChannelDescription] = useState('');

const user = hostInfo.filter(user => user.id === Number(id));
const hostChannelId = Number(id);
const { data: hostInfo } = useHostChannelInfo(hostChannelId);
const { mutate } = useUpdateHostChannelInfo();
const queryClient = useQueryClient();

const handeHostInfoClick = () => {
setSelectedHost(true);
Expand All @@ -25,13 +31,44 @@ const HostEditPage = () => {
setSelectedInfo(true);
setSelectedHost(false);
};

const handleSave = () => {
if (!hostInfo?.result.id) return;

const updatedData = {
hostChannelId,
profileImageUrl: hostInfo.result.profileImageUrl,
hostChannelName: hostInfo.result.hostChannelName,
hostEmail: hostInfo.result.email,
channelDescription,
};

mutate(updatedData, {
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['hostInfo', hostChannelId],
});
alert('저장되었습니다.');
},
onError: () => {
alert('저장에 실패했습니다.');
},
});
};

const handleAddClick = () => {
if (inputValue.trim() && !tags.includes(inputValue.trim())) {
setTags([...tags, inputValue.trim()]);
setInputValue('');
}
};

useEffect(() => {
if (hostInfo?.result.channelDescription && channelDescription === '') {
setChannelDescription(hostInfo.result.channelDescription);
}
}, [hostInfo, channelDescription]);

return (
<HostDetailLayout>
<div className="relative overflow-y-auto">
Expand Down Expand Up @@ -61,20 +98,20 @@ const HostEditPage = () => {
<div className="flex flex-col px-8 md:px-12 gap-6">
<div className="flex flex-col gap-4 py-4">
<p className="text-xl text-black font-semibold">대표 이메일</p>
<p>example@example.com</p>
<p>{hostInfo?.result.email}</p>
</div>
<div className="flex flex-col gap-4 lg:gap-6">
<p className="text-xl text-black font-semibold">멤버 목록</p>
<div className="flex flex-wrap gap-x-5 gap-y-4 lg:gap-x-10 lg:gap-y-6 text-sm md:text-16 lg:text-base">
{user.map(user => (
{hostInfo?.result.hostChannelMembers.map(user => (
<ProfileCircle
key={user.id}
id={user.id}
profile="userProfile"
name={user.nickname}
name={user.memberName.slice(1)}
className="w-12 h-12 md:w-13 md:h-13 lg:w-14 lg:h-14 text-sm md:text-16 lg:text-base"
>
{user.name}
{user.memberName}
</ProfileCircle>
))}
</div>
Expand All @@ -87,17 +124,18 @@ const HostEditPage = () => {
<DefaultTextField
label="대표 이메일"
detail="채널 혹은, 채널에서 주최하는 이벤트에 대해 문의 할 수 있는 메일로 작성해주세요."
placeholder="example@example.com"
value={hostInfo?.result.email || ''}
className="h-12"
labelClassName="sm:text-base md:text-lg"
/>
<div className="flex flex-col gap-2">
<MultilineTextField
label="채널에 대한 설명"
placeholder="채널에 대한 설명을 작성해주세요."
value={channelDescription}
onChange={e => setChannelDescription(e.target.value)}
className="h-24 mb-8"
/>
<TertiaryButton type="button" label="저장하기" size="large" color="pink" />
<TertiaryButton type="button" label="저장하기" size="large" color="pink" onClick={handleSave} />
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-col">
Expand Down
13 changes: 7 additions & 6 deletions src/pages/menu/ui/myHost/HostInfoPage.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
import HostDetailLayout from '../../../../shared/ui/backgrounds/HostDetailLayout';
import ProfileCircle from '../../../../../design-system/ui/Profile';
import { useParams } from 'react-router-dom';
import { hostInfo } from '../../../../shared/types/hostInfoType';
import useHostChannelInfo from '../../../../entities/host/hook/useHostChannelInfoHook';

const HostInfoPage = () => {
const { id } = useParams<{ id: string }>();

const user = hostInfo.filter(user => user.id === Number(id));
const hostChannelId = Number(id);
const { data } = useHostChannelInfo(hostChannelId);

Comment on lines +9 to 11
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

URL 파라미터 검증 및 로딩/에러 상태를 명시적으로 처리해주세요
idundefined이거나 숫자로 변환이 실패(NaN)할 경우, 훅이 즉시 호출되어 불필요한 API 요청이 발생하거나 컴포넌트가 예외 상태에 빠질 수 있습니다. enabled 옵션 또는 early-return 패턴으로 방어 코드를 추가하는 편이 안전합니다. 또한 로딩·에러 UI를 명시해 UX를 개선해 보세요.

-const hostChannelId = Number(id);
-const { data } = useHostChannelInfo(hostChannelId);
+const hostChannelId = Number(id);
+
+// 잘못된 URL 파라미터 방어
+if (!id || Number.isNaN(hostChannelId)) {
+  return <div>잘못된 URL 입니다.</div>;
+}
+
+// React-Query useHostChannelInfo 훅이 `enabled` 옵션을 지원한다고 가정
+const { data, isLoading, isError } = useHostChannelInfo(hostChannelId, {
+  enabled: !!hostChannelId,
+});
+
+if (isLoading) return <div>로딩 중...</div>;
+if (isError)  return <div>데이터를 불러오지 못했습니다.</div>;
📝 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
const hostChannelId = Number(id);
const { data } = useHostChannelInfo(hostChannelId);
const hostChannelId = Number(id);
// 잘못된 URL 파라미터 방어
if (!id || Number.isNaN(hostChannelId)) {
return <div>잘못된 URL 입니다.</div>;
}
// React-Query useHostChannelInfo 훅이 `enabled` 옵션을 지원한다고 가정
const { data, isLoading, isError } = useHostChannelInfo(hostChannelId, {
enabled: !!hostChannelId,
});
if (isLoading) return <div>로딩 중...</div>;
if (isError) return <div>데이터를 불러오지 못했습니다.</div>;

return (
<HostDetailLayout>
<div className="relative overflow-y-auto">
<div className="flex flex-col px-8 md:px-12 gap-6">
<div className="flex flex-col gap-4 py-8">
<p className="text-xl text-black font-semibold">대표 이메일</p>
<p>example@example.com</p>
<p>{data?.result.email}</p>
</div>
<div className="flex flex-col gap-4 lg:gap-6">
<p className="text-xl text-black font-semibold">멤버 목록</p>
<div className="flex flex-wrap gap-x-5 gap-y-4 lg:gap-x-10 lg:gap-y-6 text-sm md:text-16 lg:text-base">
{user.map(user => (
{data?.result.hostChannelMembers.map(user => (
<ProfileCircle
key={user.id}
id={user.id}
profile="userProfile"
name={user.nickname}
name={user.memberName.slice(1)}
className="w-12 h-12 md:w-13 md:h-13 lg:w-14 lg:h-14 text-sm md:text-16 lg:text-base"
>
{user.name}
{user.memberName}
</ProfileCircle>
))}
</div>
Expand Down
11 changes: 6 additions & 5 deletions src/shared/ui/backgrounds/HostDetailLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import ProfileCircle from '../../../../design-system/ui/Profile';
import Header from '../../../../design-system/ui/Header';
import { ButtonHTMLAttributes, ReactElement } from 'react';
import { hostInfo, hostInfoData } from '../../types/hostInfoType';
import useHostChannelInfo from '../../../entities/host/hook/useHostChannelInfoHook';

interface HostDetailLayoutProps {
rightContent?: ReactElement<ButtonHTMLAttributes<HTMLButtonElement>>;
Expand All @@ -12,7 +12,8 @@ const HostDetailLayout = ({ rightContent, children }: HostDetailLayoutProps) =>
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();

const host: hostInfoData | undefined = hostInfo.find(host => host.id === Number(id));
const hostChannelId = Number(id);
const { data } = useHostChannelInfo(hostChannelId);

const handleBackClick = () => {
navigate(-1);
Expand All @@ -29,12 +30,12 @@ const HostDetailLayout = ({ rightContent, children }: HostDetailLayoutProps) =>
rightContent={rightContent}
color="white"
/>
<div className="flex justify-center items-center px-6 md:px-10">
<div className="flex justify-start items-center px-6 md:px-10">
<ProfileCircle profile="hostProfile" className="md:w-28 md:h-28 w-24 h-24" />

<div className="flex flex-col gap-1 md:gap-3 ml-5 text-white">
<p className="text-lg md:text-xl font-bold">{host?.name}</p>
<p className="flex-wrap text-sm md:text-base">{host?.description}</p>
<p className="text-lg md:text-xl font-bold">{data?.result.hostChannelName}</p>
<p className="flex-wrap text-sm md:text-base">{data?.result.channelDescription}</p>
Comment on lines +37 to +38
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

동적 데이터 렌더링 구현

정적 데이터 대신 API에서 가져온 동적 데이터를 사용하여 호스트 이름과 설명을 렌더링하도록 변경되었습니다. 이는 좋은 개선입니다.

그러나 데이터 로딩 중이거나 오류 발생 시 대체 UI가 없어 사용자 경험에 영향을 줄 수 있습니다. 로딩 상태와 오류 상태에 대한 처리를 추가하는 것을 권장합니다.

const HostDetailLayout = ({ rightContent, children }: HostDetailLayoutProps) => {
  const { id } = useParams<{ id: string }>();
  const navigate = useNavigate();

  const hostChannelId = Number(id);
-  const { data } = useHostChannelInfo(hostChannelId);
+  const { data, isLoading, error } = useHostChannelInfo(hostChannelId);

  const handleBackClick = () => {
    navigate(-1);
  };

+  // 로딩 중 또는 에러 발생 시 대체 UI 렌더링
+  const renderHostInfo = () => {
+    if (isLoading) {
+      return (
+        <div className="flex flex-col gap-1 md:gap-3 ml-5 text-white">
+          <p className="text-lg md:text-xl font-bold">로딩 중...</p>
+          <p className="flex-wrap text-sm md:text-base">호스트 정보를 불러오고 있습니다.</p>
+        </div>
+      );
+    }
+    
+    if (error) {
+      return (
+        <div className="flex flex-col gap-1 md:gap-3 ml-5 text-white">
+          <p className="text-lg md:text-xl font-bold">정보 불러오기 실패</p>
+          <p className="flex-wrap text-sm md:text-base">호스트 정보를 불러오는데 실패했습니다.</p>
+        </div>
+      );
+    }
+    
+    return (
+      <div className="flex flex-col gap-1 md:gap-3 ml-5 text-white">
+        <p className="text-lg md:text-xl font-bold">{data?.result.hostChannelName || '이름 없음'}</p>
+        <p className="flex-wrap text-sm md:text-base">{data?.result.channelDescription || '설명 없음'}</p>
+      </div>
+    );
+  };

  return (
    <div className="bg-white relative">
      <div className="top-0 h-48 md:h-56 bg-gradient-to-br from-[#FF5593] to-[rgb(255,117,119)]">
        {/* 헤더 영역 */}
        <Header
          leftButtonLabel="<"
          leftButtonClassName="text-xl z-30"
          leftButtonClick={handleBackClick}
          rightContent={rightContent}
          color="white"
        />
        <div className="flex justify-start items-center px-6 md:px-10">
          <ProfileCircle profile="hostProfile" className="md:w-28 md:h-28 w-24 h-24" />

-          <div className="flex flex-col gap-1 md:gap-3 ml-5 text-white">
-            <p className="text-lg md:text-xl font-bold">{data?.result.hostChannelName}</p>
-            <p className="flex-wrap text-sm md:text-base">{data?.result.channelDescription}</p>
-          </div>
+          {renderHostInfo()}
        </div>
      </div>
      {/* 레이아웃 내용 */}
      <div className="absolute top-[calc(100%-2vh)] w-full bg-white rounded-t-[20px]">{children}</div>
    </div>
  );
};
📝 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
<p className="text-lg md:text-xl font-bold">{data?.result.hostChannelName}</p>
<p className="flex-wrap text-sm md:text-base">{data?.result.channelDescription}</p>
const HostDetailLayout = ({ rightContent, children }: HostDetailLayoutProps) => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const hostChannelId = Number(id);
const { data, isLoading, error } = useHostChannelInfo(hostChannelId);
const handleBackClick = () => {
navigate(-1);
};
// 로딩 중 또는 에러 발생 시 대체 UI 렌더링
const renderHostInfo = () => {
if (isLoading) {
return (
<div className="flex flex-col gap-1 md:gap-3 ml-5 text-white">
<p className="text-lg md:text-xl font-bold">로딩 중...</p>
<p className="flex-wrap text-sm md:text-base">호스트 정보를 불러오고 있습니다.</p>
</div>
);
}
if (error) {
return (
<div className="flex flex-col gap-1 md:gap-3 ml-5 text-white">
<p className="text-lg md:text-xl font-bold">정보 불러오기 실패</p>
<p className="flex-wrap text-sm md:text-base">호스트 정보를 불러오는데 실패했습니다.</p>
</div>
);
}
return (
<div className="flex flex-col gap-1 md:gap-3 ml-5 text-white">
<p className="text-lg md:text-xl font-bold">{data?.result.hostChannelName || '이름 없음'}</p>
<p className="flex-wrap text-sm md:text-base">{data?.result.channelDescription || '설명 없음'}</p>
</div>
);
};
return (
<div className="bg-white relative">
<div className="top-0 h-48 md:h-56 bg-gradient-to-br from-[#FF5593] to-[rgb(255,117,119)]">
{/* 헤더 영역 */}
<Header
leftButtonLabel="<"
leftButtonClassName="text-xl z-30"
leftButtonClick={handleBackClick}
rightContent={rightContent}
color="white"
/>
<div className="flex justify-start items-center px-6 md:px-10">
<ProfileCircle profile="hostProfile" className="md:w-28 md:h-28 w-24 h-24" />
{renderHostInfo()}
</div>
</div>
{/* 레이아웃 내용 */}
<div className="absolute top-[calc(100%-2vh)] w-full bg-white rounded-t-[20px]">
{children}
</div>
</div>
);
};

</div>
</div>
</div>
Expand Down