Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guild Member Setting 페이지 #275

Merged
merged 32 commits into from
Jan 11, 2025
Merged

Guild Member Setting 페이지 #275

merged 32 commits into from
Jan 11, 2025

Conversation

sumi-0011
Copy link
Member

@sumi-0011 sumi-0011 commented Jan 10, 2025

💡 기능

  • 디자인은 모달이지만, 일단 페이지로만 개발하였어요.
  • 모달로 할지는 지우언니랑 한번 이야기해볼 예정입니다.

🔎 기타

image

Summary by CodeRabbit

릴리즈 노트

  • 새로운 기능

    • 길드 시스템 도입
      • 길드 생성, 검색, 상세 보기 기능 추가
      • 길드 멤버 관리 및 초대 기능 구현
      • 길드 설정 및 프로필 편집 지원
  • 개선 사항

    • 사용자 경험을 향상시키는 인터셉터 및 모달 컴포넌트 추가
    • 페이지네이션 및 검색 기능 최적화
    • 다국어 지원 확장
  • 버그 수정

    • 인터셉터 로직 개선
    • 라우팅 및 상태 관리 안정성 향상
  • 기타

    • 의존성 업데이트
    • 코드 구조 리팩토링

Copy link

coderabbitai bot commented Jan 10, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

개요

워크스루

이 풀 리퀘스트는 길드(Guild) 기능의 전면적인 구현을 포함하고 있습니다. 새로운 컴포넌트, API 엔드포인트, 라우팅 로직, 그리고 사용자 인터페이스 요소들이 추가되었습니다. 길드 생성, 가입, 멤버 관리, 세부 정보 표시 등 길드와 관련된 다양한 기능들이 구현되었습니다.

변경 사항

파일 변경 요약
apps/web/messages/en_US.json 길드 관련 메시지 추가
apps/web/messages/ko_KR.json 길드 관련 한국어 메시지 추가
apps/web/src/components/GNB/menu.constants.tsx 길드 네비게이션 메뉴 추가
packages/api/src/guild/* 길드 관련 API 엔드포인트 및 스키마 구현
apps/web/src/app/[locale]/guild/* 길드 페이지 및 컴포넌트 구현

시퀀스 다이어그램

sequenceDiagram
    participant User
    participant GuildCreatePage
    participant CreateGuildAPI
    participant Router

    User->>GuildCreatePage: 길드 생성 페이지 접속
    User->>GuildCreatePage: 길드 정보 입력
    User->>GuildCreatePage: 페르소나 선택
    GuildCreatePage->>CreateGuildAPI: 길드 생성 요청
    CreateGuildAPI-->>GuildCreatePage: 생성 성공
    GuildCreatePage->>Router: 새로 생성된 길드 페이지로 이동
Loading

관련 가능성 있는 PR들

제안된 라벨

diff: M

제안된 리뷰어

hyesungoh

시 (Poem)

토끼가 만드는 길드의 세계
코드로 짓는 우리의 이야기
페르소나 모여 하나 되어
깃허브 위에 피어나는 꿈
🐰✨ 길드, 우리의 새로운 시작!


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 50

🔭 Outside diff range comments (1)
apps/web/src/app/[locale]/shop/AuctionSection/HistoryTable.tsx (1)

Line range hint 22-26: 데이터 처리 로직 개선이 필요합니다.

현재 구현에서 다음 사항들을 고려해보시면 좋겠습니다:

  1. 데이터 페칭 에러 처리가 없습니다
  2. 데이터가 없는 경우의 처리가 명확하지 않습니다

예시 구현:

  const { data } = useQuery({
    ...auctionQueries.historyOptions({ pageNumber: currentPage, ...searchOptions }),
    placeholderData: (prevData) => prevData,
+   onError: (error) => {
+     // 에러 처리 로직 추가
+   },
  });

+ const NoDataView = () => (
+   <div className={tbodyCss}>
+     <div>{t('no_history_data')}</div>
+   </div>
+ );

  return (
    <>
      <div className={tableCss}>
        {/* ... thead ... */}
        <div className={tbodyCss}>
          {!data && Array.from({ length: 8 }).map((_, index) => <ShopTableRowViewSkeleton key={`skeleton-${index}`} />)}
+         {data?.products.length === 0 && <NoDataView />}
          {data?.products.map((product) => {
            // ... 현재 구현 ...
          })}
        </div>
      </div>
    </>
  );
🧹 Nitpick comments (55)
packages/api/src/auction/getProductHistories.ts (1)

Line range hint 9-12: 상수 타입을 별도의 스키마로 분리하는 것을 고려해보세요.

orderTypesortDirection에 대한 유니온 타입이 getProducts.ts의 스키마와 중복됩니다. 이러한 상수 타입을 공통 스키마로 분리하면 코드 재사용성이 향상될 것 같습니다.

apps/web/src/app/[locale]/shop/AuctionSection/SellSection/PetList.tsx (1)

30-30: 상태 표현 방식이 개선되었습니다!

불리언 값 대신 문자열 상태를 사용하여 컴포넌트의 상태를 더 명확하게 표현하고 있습니다. 향후 확장성을 위해 status 타입을 상수나 enum으로 정의하는 것을 고려해보세요.

// 예시: 상수로 정의
const BANNER_STATUS = {
  SELECTED: 'selected',
  DEFAULT: 'default'
} as const;

type BannerStatus = typeof BANNER_STATUS[keyof typeof BANNER_STATUS];
apps/web/src/app/[locale]/mypage/PersonaList.tsx (1)

88-93: 상태 처리와 로딩 상태가 잘 구현되어 있습니다!

Banner 컴포넌트의 상태 처리가 일관성 있게 구현되어 있고, 로딩 상태도 적절히 처리되어 있습니다. 성능 최적화를 위해 status 값을 useMemo로 캐싱하는 것을 고려해보세요.

const bannerStatus = useMemo(
  () => (isSelected ? 'selected' : 'default'),
  [isSelected]
);
apps/web/src/app/[locale]/shop/AuctionSection/PersonaSearch.tsx (1)

95-95: 중복 로직을 컴포넌트로 분리하면 좋을 것 같습니다.

이벤트 페르소나와 일반 페르소나에서 Banner 컴포넌트를 동일한 방식으로 사용하고 있습니다. 코드 재사용성을 높이기 위해 별도의 컴포넌트로 분리하는 것을 추천드립니다.

interface PersonaBannerProps {
  type: string;
  selected?: string;
  onClick: (type: string) => void;
}

const PersonaBanner = ({ type, selected, onClick }: PersonaBannerProps) => (
  <button onClick={() => onClick(type)}>
    <Banner
      image={getPersonaImage(type)}
      status={selected === type ? 'selected' : 'default'}
    />
  </button>
);

Also applies to: 107-110

apps/web/src/app/[locale]/shop/AuctionSection/HistoryTable.tsx (1)

Line range hint 35-38: 날짜 포맷팅 로직 개선이 필요합니다.

현재 문자열 조작으로 처리되는 날짜 포맷팅을 개선하는 것이 좋겠습니다. date-fns 또는 dayjs와 같은 라이브러리를 사용하면 더 안정적이고 유지보수가 쉬운 코드가 될 것 같습니다.

예시 구현:

- const getHistoryActionLabel = (soldAt: string) => {
-   return String(soldAt)?.slice(2, 10).replace(/-/g, '.');
- };
+ import { format } from 'date-fns';
+ 
+ const getHistoryActionLabel = (soldAt: string) => {
+   return format(new Date(soldAt), 'yy.MM.dd');
+ };
apps/web/src/serverActions/guild.ts (1)

28-55: 에러 처리와 로깅을 개선해주세요.

다음 사항들의 개선이 필요합니다:

  1. 중복된 console.log 제거
  2. 주석 처리된 redirect 로직에 대한 결정
  3. 에러 메시지 한글화
   try {
     await updateGuild(guildId, {
       title,
       body,
       farmType,
       guildIcon,
       autoJoin: true,
     });

-    // redirect(`/guild/${guildId}`);
-    return { message: 'Guild updated', success: true };
+    return { message: '길드가 업데이트되었습니다', success: true };
   } catch (error) {
-    console.log('error: ', error);
-    console.log('error: ', (error as Error).message);
+    console.error('길드 업데이트 중 오류 발생:', error);
-    return { message: 'Failed to update guild', success: false };
+    return { message: '길드 업데이트에 실패했습니다', success: false };
   }
packages/api/src/_instance/safe.ts (1)

3-3: import 경로 최적화 검토 필요

CustomException import 경로가 매우 구체적으로 지정되어 있습니다. 이는 빌드 최적화에 영향을 미칠 수 있습니다.

제안사항:
패키지 레벨에서 export하도록 수정하여 import 경로를 단순화하는 것을 고려해보세요.

-import { CustomException } from '@gitanimals/exception/src/CustomException';
+import { CustomException } from '@gitanimals/exception';
packages/api/src/guild/kickMemberFromGuild.ts (1)

4-9: API 요청 구조와 타입 정의가 명확합니다.

zod를 사용한 스키마 검증이 잘 구현되어 있습니다. 다만, API 엔드포인트에서 사용되는 파라미터 네이밍 컨벤션을 일관성 있게 유지하는 것이 좋겠습니다.

- 'user-id': request.userId,
+ userId: request.userId,

이렇게 수정하면 전체 API에서 일관된 네이밍 컨벤션을 유지할 수 있습니다.

Also applies to: 11-17

packages/api/src/guild/getGuildIcons.ts (1)

6-11: 응답 타입 정의에 대한 개선 제안

GetGuildIconsResponse 타입이 IconSchema에서 직접 추론되고 있는데, GetGuildIconsResponseSchema에서 추론하는 것이 더 명확할 것 같습니다.

- export type GetGuildIconsResponse = z.infer<typeof IconSchema>[];
+ export type GetGuildIconsResponse = z.infer<typeof GetGuildIconsResponseSchema>;
packages/api/src/guild/getGuildBackgrounds.ts (1)

9-15: 응답 타입 정의 개선이 필요합니다.

getGuildIcons.ts와 동일한 패턴으로, 응답 타입 정의를 개선하면 좋을 것 같습니다.

- export type GetGuildBackgroundsResponse = z.infer<typeof BackgroundSchema>[];
+ export type GetGuildBackgroundsResponse = z.infer<typeof GetGuildBackgroundsResponseSchema>;
apps/web/src/components/GNB/menu.constants.tsx (1)

17-21: 길드 메뉴 아이콘 및 순서 검토 필요

  1. 길드를 나타내는 아이콘으로 HouseIcon보다 Users 또는 Group 아이콘이 더 직관적일 것 같습니다.
  2. 메뉴 순서가 적절한지 검토가 필요합니다. 현재 shop 다음에 위치해 있는데, 사용자 경험 관점에서 이 순서가 최적인지 확인해주세요.
   {
     label: 'guild',
     href: '/guild',
-    icon: <HouseIcon size={20} color="#9295A1" />,
+    icon: <UsersIcon size={20} color="#9295A1" />,
   },
packages/api/src/guild/joinGuild.ts (1)

14-16: API 엔드포인트 설계 및 에러 처리 개선 필요

  1. 현재 엔드포인트가 RESTful 하지 않습니다. /guilds/{guildId}/join이 더 적절할 것 같습니다.
  2. 실패 케이스에 대한 명시적인 에러 처리가 필요합니다.

다음과 같은 개선을 제안합니다:

 export const joinGuild = async (request: JoinGuildRequest): Promise<JoinGuildResponse> => {
-  return renderPost(`/guilds/${request.guildId}`, { personaId: request.personaId });
+  try {
+    return await renderPost(
+      `/guilds/${request.guildId}/join`,
+      { personaId: request.personaId }
+    );
+  } catch (error) {
+    if (error instanceof ApiError) {
+      switch (error.status) {
+        case 403:
+          throw new Error('이미 길드에 가입되어 있습니다.');
+        case 404:
+          throw new Error('길드를 찾을 수 없습니다.');
+        default:
+          throw new Error('길드 가입 중 오류가 발생했습니다.');
+      }
+    }
+    throw error;
+  }
 };
apps/web/src/app/[locale]/guild/layout.tsx (1)

15-26: 스타일 구현에 대한 제안사항이 있습니다.

현재 구현은 전반적으로 잘 되어 있으나, 다음과 같은 개선사항을 고려해보시기 바랍니다:

  1. calc(100vh - 60px)의 매직 넘버를 상수로 분리하는 것이 좋습니다.
  2. mobile 스타일의 100vh와 공백 문자 오타를 수정해야 합니다.

다음과 같이 수정하는 것을 추천드립니다:

const HEADER_HEIGHT = '60px';

const containerStyle = css({
  width: '100%',
  background: 'linear-gradient(180deg, #000 0%, #004875 38.51%, #005B93 52.46%, #006FB3 73.8%, #0187DB 100%)',
  minHeight: 'fit-content',
-  height: 'calc(100vh - 60px)',
+  height: `calc(100vh - ${HEADER_HEIGHT})`,
  overflow: 'hidden',
  position: 'relative',

  _mobile: {
-    minHeight: '100vh ',
+    minHeight: '100vh',
  },
});
packages/api/src/guild/createGuild.ts (1)

20-22: 에러 처리 개선이 필요합니다.

API 호출 시 발생할 수 있는 에러에 대한 처리가 부족합니다. 사용자에게 더 명확한 에러 메시지를 제공하는 것이 좋습니다.

다음과 같은 에러 처리 개선을 제안드립니다:

export const createGuild = (request: CreateGuildRequest): Promise<CreateGuildResponse> => {
-  return safeRenderPost(CreateGuildResponseSchema)('/guilds', request);
+  return safeRenderPost(CreateGuildResponseSchema)('/guilds', request)
+    .catch((error) => {
+      if (error.response?.status === 409) {
+        throw new Error('이미 존재하는 길드 이름입니다');
+      }
+      throw new Error('길드 생성 중 오류가 발생했습니다. 다시 시도해주세요');
+    });
};
apps/web/src/app/[locale]/guild/[id]/setting/layout.tsx (1)

7-23: 시맨틱 HTML 구조 개선이 필요합니다

현재 일반 div 태그들로만 구성되어 있습니다. 접근성과 시맨틱한 구조를 위해 다음과 같이 수정하는 것을 추천드립니다:

-    <div className={css({ minH: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' })}>
+    <main className={css({ minH: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' })}>
-      <div
+      <section
         className={css({
           maxWidth: '880px',
           mx: 'auto',
           background: 'gray.gray_150',
           p: '40px',
           borderRadius: '16px',
           color: 'white.white',
           w: '100%',
         })}
       >
         <Flex flexDirection="column" gap="24px">
           {children}
         </Flex>
-      </div>
-    </div>
+      </section>
+    </main>
apps/web/src/app/[locale]/guild/detail/[id]/layout.tsx (2)

13-21: 디자인 토큰 사용을 권장드립니다

스타일 상수에 하드코딩된 값들이 많이 있습니다. 유지보수성과 일관성을 위해 디자인 토큰을 사용하는 것이 좋겠습니다:

  • maxWidth: '1000px'
  • padding: '20px'
  • padding: '60px 100px'
  • borderRadius: '16px'

이러한 값들을 디자인 시스템의 토큰으로 관리하면 향후 디자인 변경 시 더 효율적으로 대응할 수 있습니다.

Also applies to: 23-32


17-17: 계산된 높이값 개선이 필요합니다

minHeight: 'calc(100vh - 60px)'에서 60px이 하드코딩되어 있습니다. 이 값이 네비게이션 바의 높이라면, 상수나 CSS 변수로 관리하는 것이 좋겠습니다.

- minHeight: 'calc(100vh - 60px)',
+ minHeight: 'calc(100vh - var(--navbar-height))',
apps/web/src/components/Guild/MemeberSlider.tsx (1)

10-24: 접근성 개선이 필요합니다

슬라이더 컴포넌트에 다음과 같은 접근성 개선이 필요합니다:

  • ARIA 레이블 추가
  • 키보드 네비게이션 지원
  • 스크린 리더 지원

또한 에러 바운더리와 로딩 상태 처리도 추가하면 좋겠습니다.

packages/api/src/guild/schema.ts (1)

15-31: Guild 스키마에 추가적인 유효성 검사가 필요합니다.

title, body 등의 필드에 대한 길이 제한이나 추가적인 유효성 검사 규칙이 없습니다. 데이터 품질 보장을 위해 추가하는 것이 좋겠습니다.

export const GuildSchema = z.object({
  id: z.string(),
-  title: z.string(),
+  title: z.string().min(1).max(100),
-  body: z.string(),
+  body: z.string().max(1000),
  guildIcon: z.string(),
  leader: z.object({
    userId: z.string(),
    name: z.string(),
-    contributions: z.string(),
+    contributions: z.number(),
    personaId: z.string(),
    personaType: z.string(),
  }),
  farmType: z.string(),
-  totalContributions: z.string(),
+  totalContributions: z.number(),
  members: z.array(GuildMemberSchema),
  waitMembers: z.array(GuildMemberSchema),
  createdAt: z.string(),
});
packages/lib/react-query/src/guild/queries.ts (1)

8-11: 쿼리 설정에 캐싱 전략을 추가해주세요.

현재 쿼리 설정에 staleTime과 cacheTime이 정의되어 있지 않습니다. 성능 최적화를 위해 적절한 캐싱 전략을 추가하는 것이 좋겠습니다.

    queryOptions({
      queryKey: guildQueries.getGuildById(guildId),
      queryFn: () => getGuildById({ guildId }),
+     staleTime: 1000 * 60 * 5, // 5분
+     cacheTime: 1000 * 60 * 30, // 30분
+     retry: 3,
    }),
apps/web/src/app/[locale]/guild/[id]/@modal/(.)setting/page.tsx (3)

9-15: 데이터 페칭 최적화 및 에러 처리 필요

데이터 페칭 로직에 다음과 같은 개선이 필요합니다:

  1. 권한 체크와 데이터 페칭을 병렬로 처리하여 성능을 개선할 수 있습니다.
  2. 데이터 페칭 실패 시 에러 처리가 누락되어 있습니다.
 export default async function GuildSettingModal({ params }: { params: { id: string } }) {
-  const isLeader = await checkIsLeader(params.id);
-
-  const icons = await getGuildIcons();
-  const backgrounds = await getGuildBackgrounds();
-  const data = await getGuildById({ guildId: params.id });
+  const [isLeader, icons, backgrounds, data] = await Promise.all([
+    checkIsLeader(params.id),
+    getGuildIcons(),
+    getGuildBackgrounds(),
+    getGuildById({ guildId: params.id }).catch(error => {
+      console.error('Failed to fetch guild data:', error);
+      throw error;
+    })
+  ]);

16-18: 권한 체크 위치 최적화 필요

권한 체크를 데이터 페칭 이후에 수행하면 불필요한 API 호출을 방지할 수 있습니다.


20-26: 타입 안전성 개선 필요

컴포넌트 props의 타입 정의가 누락되어 있습니다. 타입스크립트의 이점을 최대한 활용하기 위해 props 타입을 명시적으로 정의하는 것이 좋습니다.

apps/web/src/app/[locale]/guild/(components)/GuildPetSelectDialog.tsx (1)

17-21: 에러 처리 보완 필요

onDone 함수에서 selectPersona가 없는 경우에 대한 사용자 피드백이 누락되어 있습니다.

   const onDone = () => {
     if (selectPersona) {
       onSubmit(selectPersona);
+    } else {
+      toast.error(t('guild.select_persona_required'));
     }
   };
apps/web/src/app/[locale]/guild/[id]/setting/member/MemberCard.tsx (1)

22-30: 접근성 개선 필요

Remove 버튼에 적절한 aria-label이 누락되어 있습니다.

       bottomElement={
-        <Button onClick={kickMember}>Remove</Button>
+        <Button 
+          onClick={kickMember}
+          aria-label={t('guild.remove_member_aria_label', { name: member.name })}
+        >
+          {t('guild.remove_member')}
+        </Button>
       }
apps/web/src/app/[locale]/guild/[id]/setting/member/BannerGuildMember.tsx (1)

27-35: 반응형 디자인 보완 필요

반응형 디자인을 위한 미디어 쿼리가 누락되어 있습니다.

 const bannerStyle = css({
   display: 'inline-flex',
   padding: '8px 16px 16px 16px',
   flexDirection: 'column',
   alignItems: 'center',
   gap: '12px',
   backgroundColor: 'white.white_10',
   borderRadius: '8px',
+  '@media (max-width: 768px)': {
+    padding: '4px 8px 8px 8px',
+    gap: '8px',
+  }
 });
apps/web/src/components/InterceptingDialog.tsx (2)

20-27: useEffect 로직 최적화 필요

현재 useEffect 내의 조건문이 불필요하게 복잡합니다. 더 간단한 구현이 가능합니다.

다음과 같이 변경하는 것을 추천드립니다:

  useEffect(() => {
-   if (!isOpen) {
-     setIsOpen(true);
-   } else {
-     setIsOpen(false);
-   }
+   setIsOpen(!isOpen);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [pathname]);

30-34: 접근성 속성 추가 필요

Dialog 컴포넌트에 접근성 관련 속성이 부족합니다.

다음 속성들의 추가를 고려해주세요:

  <Dialog open={isOpen} onOpenChange={onClose}>
-   <Dialog.Content size="large" className={dialogContentStyle}>
+   <Dialog.Content 
+     size="large" 
+     className={dialogContentStyle}
+     aria-describedby="dialog-description"
+     role="dialog"
+   >
packages/api/src/guild/searchGuild.ts (1)

7-8: 필터 타입 개선 제안

현재 필터 타입이 매직 스트링으로 되어있어 유지보수가 어려울 수 있습니다.

상수로 분리하여 관리하는 것을 추천드립니다:

+ export const FILTER_TYPES = {
+   RANDOM: 'RANDOM',
+   PEOPLE_ASC: 'PEOPLE_ASC',
+   PEOPLE_DESC: 'PEOPLE_DESC',
+   CONTRIBUTION_ASC: 'CONTRIBUTION_ASC',
+   CONTRIBUTION_DESC: 'CONTRIBUTION_DESC',
+ } as const;
- const FilterSchema = z.enum(['RANDOM', 'PEOPLE_ASC', 'PEOPLE_DESC', 'CONTRIBUTION_ASC', 'CONTRIBUTION_DESC']);
+ const FilterSchema = z.enum(Object.values(FILTER_TYPES));
apps/web/src/app/[locale]/guild/(components)/GuildSearch.tsx (1)

31-42: 접근성 및 사용성 개선 필요

검색 컴포넌트의 접근성이 부족합니다.

다음과 같은 개선사항을 제안드립니다:

  return (
    <>
      {text && (
-       <button onClick={onSearchReset}>
+       <button 
+         onClick={onSearchReset}
+         aria-label="검색 초기화"
+         className="reset-button"
+       >
          <ChevronLeftIcon size="28px" color="#FFFFFF80" />
        </button>
      )}

      <Box flex="1">
        <SearchBar 
          defaultValue={text} 
          onChange={(e) => setInput(e.target.value)} 
          onSubmit={onSubmit}
+         placeholder="길드 검색..."
+         aria-label="길드 검색 입력창"
        />
      </Box>
    </>
  );
apps/web/src/app/[locale]/guild/@modal/GuildModal.tsx (2)

14-28: 상태 관리 로직 개선 필요

현재 상태 관리 로직이 복잡하고 불필요한 리렌더링을 발생시킬 수 있습니다.

다음과 같은 개선을 제안드립니다:

- const [isOpen, setIsOpen] = useState(false);
+ const [isOpen, setIsOpen] = useState(true);

  useEffect(() => {
-   if (!isOpen) {
-     setIsOpen(true);
-   } else {
-     setIsOpen(false);
-   }
+   setIsOpen(true);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [pathname]);

29-36: 로딩 상태 처리 추가 필요

모달이 열릴 때 컨텐츠 로딩 상태를 표시하지 않고 있습니다.

로딩 상태를 추가하는 것을 추천드립니다:

+ const [isLoading, setIsLoading] = useState(false);

  return (
    <Dialog open={isOpen} onOpenChange={onClose}>
      <Dialog.Content size="large" className={dialogContentStyle}>
        {title && <Dialog.Title>{title}</Dialog.Title>}
+       {isLoading ? (
+         <LoadingSpinner />
+       ) : (
          {children}
+       )}
      </Dialog.Content>
    </Dialog>
  );
apps/web/src/app/[locale]/guild/[id]/layout.tsx (2)

7-16: 이미지 최적화 개선 필요

배경 이미지 구현에 다음과 같은 최적화가 필요합니다:

  1. 큰 이미지(3600x228)의 경우 성능에 영향을 줄 수 있습니다.
  2. 반응형 디자인을 위한 이미지 크기 최적화가 필요합니다.

다음과 같은 최적화를 제안합니다:

-        <Image src="/guild/guild-bg-bottom.webp" className="bg-bottom" alt="bg-bottom" width={3600} height={228} />
+        <Image
+          src="/guild/guild-bg-bottom.webp"
+          className="bg-bottom"
+          alt="길드 배경 이미지"
+          width={3600}
+          height={228}
+          sizes="100vw"
+          priority
+        />

34-53: 모바일 환경 대응 필요

현재 배경 이미지 스타일링이 모바일 환경에서 최적화되어 있지 않습니다.

다음과 같은 반응형 스타일 적용을 제안합니다:

const bottomBgStyle = css({
  position: 'absolute',
  width: '100vw',
  bottom: 0,
  left: '50%',
  transform: 'translateX(-50%)',

  '& .bg-bottom': {
    height: '228px',
    objectFit: 'cover',
  },
  '& .bg-bottom-house': {
    position: 'absolute',
    bottom: '32px',
    right: '62px',
    height: '202px',
    width: 'auto',
    objectFit: 'contain',
+   '@media (max-width: 768px)': {
+     right: '20px',
+     height: '150px',
+   },
  },
});
apps/web/src/app/[locale]/guild/[id]/setting/GuildSetting.tsx (1)

46-46: 에러 메시지 UI 개선 필요

현재 에러 메시지 표시 방식이 사용자 경험을 저해할 수 있습니다.

-      {state?.message && <p aria-live="polite">{state?.message}</p>}
+      {state?.message && (
+        <p
+          aria-live="polite"
+          className={css({
+            color: 'red.500',
+            mt: 2,
+            textAlign: 'center',
+            fontWeight: 'medium'
+          })}
+        >
+          {state?.message}
+        </p>
+      )}
apps/web/src/app/[locale]/guild/(components)/SortSelect.tsx (2)

22-22: 타입 안전성 개선 필요

FilterType이 undefined일 경우의 처리가 암시적으로 이루어지고 있습니다. 명시적인 타입 가드를 추가하는 것이 좋습니다.

- const filter = (searchParams.get('filter') as FilterType) ?? 'RANDOM';
+ const filterParam = searchParams.get('filter');
+ const filter: FilterType = filterParam && Object.values(FilterType).includes(filterParam as FilterType)
+   ? (filterParam as FilterType)
+   : 'RANDOM';

10-16: 상수 분리 및 국제화 적용 필요

정렬 옵션의 레이블이 하드코딩되어 있습니다. 이를 국제화하고 상수로 분리하는 것이 좋습니다.

+ import { useTranslations } from 'next-intl';
+
+ const SORT_OPTIONS = ['RANDOM', 'PEOPLE_ASC', 'PEOPLE_DESC', 'CONTRIBUTION_ASC', 'CONTRIBUTION_DESC'] as const;
+
  export function SortSelect() {
+   const t = useTranslations('guild.sort');
    const options = SORT_OPTIONS.map((value) => ({
-     label: 'Random',  // 하드코딩된 레이블
+     label: t(value.toLowerCase()),
      value,
    }));
    // ...
  }
apps/web/src/app/[locale]/guild/@modal/(.)detail/[id]/join/page.tsx (1)

43-43: 빈 스타일 객체 제거 필요

빈 CSS 스타일 객체가 정의되어 있습니다. 필요한 스타일이 없다면 제거하는 것이 좋습니다.

- const dialogTitleStyle = css({});
apps/web/src/app/[locale]/guild/[id]/MoreMenu.tsx (2)

8-10: 영문 주석으로 변경 필요

한글로 작성된 주석이 있습니다. 국제적인 협업을 위해 영문으로 변경하는 것이 좋습니다.

- /**
-  * 더보기 메뉴
-  * 현재는 모두 길드장만 볼 수 있음
-  */
+ /**
+  * More menu
+  * Currently only accessible to guild leader
+  */

12-14: 로딩 상태 처리 필요

isLeader 체크 시 로딩 상태 처리가 필요합니다.

+ import { Suspense } from 'react';
+ import { LoadingSpinner } from '@/components/LoadingSpinner';

  export async function MoreMenu({ guildId }: { guildId: string }) {
+   return (
+     <Suspense fallback={<LoadingSpinner size="sm" />}>
+       <MoreMenuContent guildId={guildId} />
+     </Suspense>
+   );
  }
apps/web/src/app/[locale]/guild/detail/[id]/join/page.tsx (1)

15-28: 로딩 상태 처리 필요

submitJoinGuild 함수 실행 중 로딩 상태를 표시하여 사용자 경험을 개선해야 합니다.

export default function GuildJoinPage({ params }: { params: { id: string } }) {
  const queryClient = useQueryClient();
  const router = useRouter();
+ const [isLoading, setIsLoading] = useState(false);

  const submitJoinGuild = async (selectPersona: string) => {
+   setIsLoading(true);
    try {
      await joinGuildAction({
        guildId: params.id,
        personaId: selectPersona,
      });
      router.replace(`/guild`);
      queryClient.invalidateQueries({ queryKey: inboxQueries.allKey() });
    } catch (error) {
      console.error(error);
+   } finally {
+     setIsLoading(false);
    }
  };
apps/web/src/app/[locale]/guild/(components)/GuildCreate.tsx (1)

48-50: 하드코딩된 가격 값 개선 필요

버튼 텍스트에 하드코딩된 가격 값이 있습니다. 상수나 설정 값으로 관리하는 것이 좋습니다.

+ const GUILD_CREATION_COST = 100_000;

- Create / 100,000P
+ Create / {GUILD_CREATION_COST.toLocaleString()}P
apps/web/src/components/Pagination/PaginationServer.tsx (1)

28-29: 하드코딩된 색상 값 개선 필요

색상 값이 하드코딩되어 있습니다. 테마 시스템을 활용하는 것이 좋습니다.

- <ChevronLeft color="#B5B8C0" />
+ <ChevronLeft color="token(colors.white.white_50)" />

- <ChevronRight color="#B5B8C0" />
+ <ChevronRight color="token(colors.white.white_50)" />

Also applies to: 48-49

apps/web/src/app/[locale]/guild/(components)/SelecableFormItem.tsx (1)

44-44: 접근성 및 스타일링 개선 필요

hidden input의 구현 방식을 개선해야 합니다.

- <input type="text" name={props.name} className="display-none" value={value ?? ''} hidden />
+ <input
+   type="text"
+   name={props.name}
+   value={value ?? ''}
+   hidden
+   aria-hidden="true"
+ />
apps/web/src/components/Gitanimals.tsx (2)

89-91: 이미지 에러 처리와 접근성 개선이 필요합니다.

다음 사항들을 개선하면 좋을 것 같습니다:

  1. 이미지 로드 실패 시 대체 처리
  2. 더 구체적인 alt 텍스트 (예: {guildId} 길드의 이미지)
-  return <img src={`https://render.gitanimals.org/guilds/${guildId}/draw`} alt="gitanimals" />;
+  return (
+    <img
+      src={`https://render.gitanimals.org/guilds/${guildId}/draw`}
+      alt={`${guildId} 길드의 이미지`}
+      onError={(e) => {
+        e.currentTarget.src = '/default-guild-image.png';
+      }}
+    />
+  );

93-102: 템플릿 리터럴의 들여쓰기를 조정해주세요.

현재 들여쓰기가 HTML 출력에 영향을 줄 수 있습니다. 불필요한 공백을 제거하는 것이 좋겠습니다.

-  return `<a href="https://www.gitanimals.org/">
-      <img
-        src="https://render.gitanimals.org/guilds/${guildId}/draw"
-        width="${sizes[0]}"
-        height="${sizes[1]}"
-        alt="gitanimals"
-      />
-    </a>`;
+  return `<a href="https://www.gitanimals.org/"><img src="https://render.gitanimals.org/guilds/${guildId}/draw" width="${sizes[0]}" height="${sizes[1]}" alt="gitanimals" /></a>`;
apps/web/src/app/[locale]/guild/[id]/page.tsx (1)

44-44: 매직 넘버를 상수로 추출해주세요.

길드 최대 인원수(15)가 하드코딩되어 있습니다. 이 값을 상수로 추출하면 유지보수가 더 용이해질 것 같습니다.

+const MAX_GUILD_MEMBERS = 15;
+
 export default async function GuildPage({ params }: { params: { id: string } }) {
   // ...
-              <span>{data.members.length}/ 15</span>
+              <span>{data.members.length}/ {MAX_GUILD_MEMBERS}</span>
apps/web/src/app/[locale]/guild/(components)/GuildCreateForm.tsx (1)

36-50: 접근성 개선과 중복된 key prop 제거가 필요합니다.

  1. 버튼에 aria-label을 추가하여 접근성을 개선해주세요.
  2. img 태그에서 중복된 key prop을 제거해주세요.
 {icons?.map((icon) => (
-  <button onClick={() => onDataChange('icon', icon)} key={icon}>
+  <button
+    onClick={() => onDataChange('icon', icon)}
+    key={icon}
+    aria-label={`길드 아이콘 ${icon} 선택`}
+  >
     <img
       src={icon}
       className={itemStyle}
       width={70}
       height={70}
-      key={icon}
       alt={icon}
       style={{
         border: formData.icon === icon ? '1.5px solid' : 'none',
       }}
     />
   </button>
 ))}
apps/web/src/app/[locale]/guild/(components)/SelectPersonaList.tsx (1)

51-51: 페르소나 정렬 기능 구현이 필요합니다.

TODO 주석에 명시된 정렬 기능을 구현하는 것을 도와드릴까요? 레벨이나 획득 날짜 기준으로 정렬하는 것이 좋을 것 같습니다.

apps/web/src/app/[locale]/guild/(components)/GuildDetail.tsx (1)

46-46: 하드코딩된 값을 상수로 분리

멤버 수 제한(15)이 하드코딩되어 있습니다. 이 값을 상수로 분리하여 관리하는 것이 좋습니다.

+const MAX_MEMBERS = 15;
+
 export const GuildDetail = wrap
   // ...
-                <span>{data.members.length}/ 15</span>
+                <span>{data.members.length}/ {MAX_MEMBERS}</span>
apps/web/src/app/[locale]/guild/(components)/GuildCard.tsx (1)

43-43: 하드코딩된 값을 상수로 분리

멤버 수 제한(15)이 하드코딩되어 있습니다. 이 값을 상수로 분리하여 재사용성을 높이는 것이 좋습니다.

apps/web/src/app/[locale]/guild/page.tsx (4)

27-36: 주석 처리된 코드를 정리해주세요.

더 이상 필요하지 않은 주석 처리된 코드는 제거하는 것이 좋습니다. 특히 리다이렉션 로직이 필요하지 않다면 관련 주석을 모두 제거해주세요.

 export default async function GuildPage({ searchParams }: GuildPageProps) {
   const allJoinGuilds = await getAllJoinGuilds();

-  // if (allJoinGuilds.guilds.length === 0) {
   return <GuildMain searchParams={searchParams} />;
-  // }

-  // const guildId = allJoinGuilds.guilds[0].id;
-  // redirect(`/guild/${guildId}`);
 }

38-79: 코드 스타일 개선이 필요합니다.

  1. 불필요한 CSS 주석을 제거해주세요.
  2. 랜덤 ID 생성 로직을 별도의 유틸리티 함수로 분리하는 것이 좋을 것 같습니다.
 const cardListStyle = grid({
   gridTemplateRows: 'repeat(3, 210px)',
   columns: 3,
   gap: '8px',
   w: 'full',
-  // flex: 1,
   _mobile: {
     columns: 1,
   },
 });

81-138: 스타일 구성이 잘 되어있습니다만, 몇 가지 개선사항이 있습니다.

  1. 스타일 상수들의 이름을 더 구체적으로 지정하면 좋을 것 같습니다. 예: topStyleguildHeaderStyle
  2. 반응형 디자인 브레이크포인트를 상수로 분리하는 것을 고려해보세요.

140-149: 접근성 개선이 필요합니다.

이미지의 alt 텍스트를 번역 시스템을 통해 다국어로 제공하면 좋을 것 같습니다.

-        <Image src="/guild/empty-image.webp" alt="empty" width={80} height={80} />
+        <Image src="/guild/empty-image.webp" alt={t('Guild.empty')} width={80} height={80} />
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0afb443 and 4749ba6.

⛔ Files ignored due to path filters (4)
  • apps/web/public/guild/guild-bg-bottom-house.png is excluded by !**/*.png
  • apps/web/public/guild/guild-bg-bottom.png is excluded by !**/*.png
  • apps/web/public/guild/init-bg-bottom.png is excluded by !**/*.png
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (82)
  • apps/web/messages/en_US.json (1 hunks)
  • apps/web/messages/ko_KR.json (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/src/apis/interceptor.ts (0 hunks)
  • apps/web/src/app/[locale]/auth/page.tsx (2 hunks)
  • apps/web/src/app/[locale]/error.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/(components)/GuidlInfoForm.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/(components)/GuidlInfoFormClient.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/(components)/GuildCard.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/(components)/GuildCreate.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/(components)/GuildCreateForm.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/(components)/GuildDetail.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/(components)/GuildPetSelectDialog.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/(components)/GuildSearch.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/(components)/SelecableFormItem.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/(components)/SelectPersonaList.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/(components)/SortSelect.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/(list)/GuildCard.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/@modal/(.)create/page.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/@modal/(.)detail/[id]/join/page.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/@modal/(.)detail/[id]/page.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/@modal/GuildModal.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/@modal/default.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/[id]/@modal/(.)setting/page.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/[id]/@modal/default.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/[id]/MoreMenu.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/[id]/layout.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/[id]/page.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/[id]/setting/GuildSetting.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/[id]/setting/layout.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/[id]/setting/member/BannerGuildMember.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/[id]/setting/member/MemberCard.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/[id]/setting/member/WaitMemberCard.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/[id]/setting/member/page.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/[id]/setting/page.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/create/page.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/detail/[id]/join/page.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/detail/[id]/layout.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/detail/[id]/page.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/layout.tsx (1 hunks)
  • apps/web/src/app/[locale]/guild/page.tsx (1 hunks)
  • apps/web/src/app/[locale]/mypage/PersonaList.tsx (1 hunks)
  • apps/web/src/app/[locale]/mypage/my-pet/(merge)/MergePersona.tsx (1 hunks)
  • apps/web/src/app/[locale]/shop/AuctionSection/HistoryTable.tsx (1 hunks)
  • apps/web/src/app/[locale]/shop/AuctionSection/PersonaSearch.tsx (2 hunks)
  • apps/web/src/app/[locale]/shop/AuctionSection/ProductTable.tsx (1 hunks)
  • apps/web/src/app/[locale]/shop/AuctionSection/SellSection/PetList.tsx (1 hunks)
  • apps/web/src/app/[locale]/shop/SellListSection/index.tsx (1 hunks)
  • apps/web/src/app/layout.tsx (2 hunks)
  • apps/web/src/components/GNB/GNB.tsx (1 hunks)
  • apps/web/src/components/GNB/menu.constants.tsx (2 hunks)
  • apps/web/src/components/Gitanimals.tsx (5 hunks)
  • apps/web/src/components/Global/SessionLoader.tsx (2 hunks)
  • apps/web/src/components/Guild/MemeberSlider.tsx (1 hunks)
  • apps/web/src/components/InterceptingDialog.tsx (1 hunks)
  • apps/web/src/components/Pagination/PaginationServer.tsx (1 hunks)
  • apps/web/src/hooks/useGetNewUrl.tsx (1 hunks)
  • apps/web/src/serverActions/guild.ts (1 hunks)
  • packages/api/src/_instance/render.ts (2 hunks)
  • packages/api/src/_instance/safe.ts (1 hunks)
  • packages/api/src/auction/getMyProducts.ts (1 hunks)
  • packages/api/src/auction/getProductHistories.ts (1 hunks)
  • packages/api/src/auction/getProducts.ts (1 hunks)
  • packages/api/src/auction/schema.ts (0 hunks)
  • packages/api/src/guild/acceptJoinGuild.ts (1 hunks)
  • packages/api/src/guild/createGuild.ts (1 hunks)
  • packages/api/src/guild/denyJoinGuild.ts (1 hunks)
  • packages/api/src/guild/getAllJoinGuilds.ts (1 hunks)
  • packages/api/src/guild/getGuildBackgrounds.ts (1 hunks)
  • packages/api/src/guild/getGuildById.ts (1 hunks)
  • packages/api/src/guild/getGuildIcons.ts (1 hunks)
  • packages/api/src/guild/index.ts (1 hunks)
  • packages/api/src/guild/joinGuild.ts (1 hunks)
  • packages/api/src/guild/kickMemberFromGuild.ts (1 hunks)
  • packages/api/src/guild/schema.ts (1 hunks)
  • packages/api/src/guild/searchGuild.ts (1 hunks)
  • packages/api/src/guild/updateGuild.ts (1 hunks)
  • packages/api/src/index.ts (2 hunks)
  • packages/api/src/schema.ts (1 hunks)
  • packages/lib/react-query/src/guild/index.ts (1 hunks)
  • packages/lib/react-query/src/guild/queries.ts (1 hunks)
  • packages/lib/react-query/src/index.ts (1 hunks)
⛔ Files not processed due to max files limit (20)
  • packages/ui/panda/package.json
  • packages/ui/panda/src/components/Banner/BannerPetSelect.tsx
  • packages/ui/panda/src/components/Banner/cva.ts
  • packages/ui/panda/src/components/Banner/index.ts
  • packages/ui/panda/src/components/Chip/CombineChip/CombineChip.tsx
  • packages/ui/panda/src/components/Chip/CombineChip/index.ts
  • packages/ui/panda/src/components/Chip/index.ts
  • packages/ui/panda/src/components/Dialog/Dialog.styles.ts
  • packages/ui/panda/src/components/Dialog/Dialog.tsx
  • packages/ui/panda/src/components/Dialog/index.ts
  • packages/ui/panda/src/components/DropdownMenu/DropdownMenu.tsx
  • packages/ui/panda/src/components/DropdownMenu/index.tsx
  • packages/ui/panda/src/components/SearchBar/SearchBar.tsx
  • packages/ui/panda/src/components/SearchBar/index.ts
  • packages/ui/panda/src/components/Select/index.ts
  • packages/ui/panda/src/components/Textfield/TextField.tsx
  • packages/ui/panda/src/components/Textfield/Textarea.tsx
  • packages/ui/panda/src/components/Textfield/index.ts
  • packages/ui/panda/src/components/index.ts
  • packages/util/common/src/url.ts
💤 Files with no reviewable changes (2)
  • packages/api/src/auction/schema.ts
  • apps/web/src/apis/interceptor.ts
✅ Files skipped from review due to trivial changes (6)
  • packages/lib/react-query/src/guild/index.ts
  • apps/web/src/app/[locale]/guild/@modal/default.tsx
  • apps/web/src/app/[locale]/guild/[id]/@modal/default.tsx
  • apps/web/src/app/[locale]/shop/AuctionSection/ProductTable.tsx
  • apps/web/src/app/[locale]/shop/SellListSection/index.tsx
  • packages/api/src/guild/index.ts
🧰 Additional context used
🪛 Biome (1.9.4)
apps/web/src/app/[locale]/guild/[id]/setting/page.tsx

[error] 22-23: There is a suspicious semicolon in the JSX element.

This is usually the result of a typo or some refactor gone wrong.
Remove the semicolon, or move it inside a JSX element.

(lint/suspicious/noSuspiciousSemicolonInJsx)

apps/web/src/app/[locale]/guild/detail/[id]/join/page.tsx

[error] 36-37: There is a suspicious semicolon in the JSX element.

This is usually the result of a typo or some refactor gone wrong.
Remove the semicolon, or move it inside a JSX element.

(lint/suspicious/noSuspiciousSemicolonInJsx)

🔇 Additional comments (31)
packages/api/src/auction/getMyProducts.ts (1)

2-3: 스키마 임포트 경로 변경이 일관되게 적용되었습니다.

PaginationSchema를 상위 디렉토리에서 임포트하도록 변경한 것이 적절해 보입니다. 다른 관련 파일들과도 일관성이 유지되었습니다.

packages/api/src/auction/getProducts.ts (1)

3-3: 스키마 임포트가 잘 구성되었습니다.

로컬 스키마와 공유 스키마의 임포트를 명확하게 분리한 것이 좋습니다. 코드의 구조가 더 명확해졌습니다.

Also applies to: 5-5

packages/api/src/auction/getProductHistories.ts (1)

2-3: 스키마 구조 개선이 일관되게 적용되었습니다.

PaginationSchema의 임포트 경로 변경이 다른 파일들과 동일한 패턴으로 적용되었습니다. 전체적인 코드 구조가 개선되었습니다.

apps/web/src/app/[locale]/mypage/my-pet/(merge)/MergePersona.tsx (1)

176-176: 메모이제이션이 잘 구현되어 있습니다!

LevelBanner 컴포넌트의 상태 처리가 일관성 있게 변경되었고, MemoizedPersonaItem의 비교 로직도 적절하게 구현되어 있습니다. 불필요한 리렌더링을 효과적으로 방지할 수 있을 것 같습니다.

apps/web/src/app/[locale]/shop/AuctionSection/HistoryTable.tsx (1)

6-6: 임포트 경로 변경이 잘 되었습니다!

컴포넌트의 정확한 위치를 지정하는 방식으로 변경되어 코드의 구조가 더 명확해졌습니다.

apps/web/src/app/layout.tsx (1)

45-46: 렌더링 전용 인터셉터의 필요성 검토 필요

일반 인터셉터와 동일한 핸들러를 사용하는 렌더링 전용 인터셉터가 추가되었습니다. 이로 인해 코드 중복이 발생하고 있습니다.

다음 사항들을 확인해 주시기 바랍니다:

  1. 렌더링 전용 인터셉터가 별도로 필요한 이유가 있나요?
  2. 일반 인터셉터와 다른 동작이 필요하다면, 각각에 맞는 별도의 핸들러를 구현하는 것이 좋지 않을까요?
  3. 만약 동일한 동작이 필요하다면, 하나의 인터셉터로 통합하는 것이 유지보수에 더 유리하지 않을까요?

다음 스크립트로 인터셉터 사용 패턴을 확인해 보겠습니다:

apps/web/src/serverActions/guild.ts (1)

1-7: 코드가 깔끔하고 잘 구성되어 있습니다!

'use server' 지시문과 import 문이 적절하게 배치되어 있으며, 타입과 함수 import가 명확하게 구분되어 있습니다.

apps/web/src/components/Global/SessionLoader.tsx (1)

23-24: 인터셉터 구현의 중복 및 문서화 필요

일반 인터셉터와 렌더 인터셉터가 동일한 핸들러를 사용하고 있습니다. 이는 잠재적인 코드 중복을 야기할 수 있습니다.

제안사항:

  1. 두 종류의 인터셉터가 필요한 이유에 대한 주석 추가
  2. 공통 로직을 별도의 유틸리티 함수로 추출 고려

다음 스크립트로 인터셉터 구현의 중복을 확인해보겠습니다:

packages/api/src/_instance/render.ts (1)

13-25: 타입 안전성이 잘 구현되었습니다

인터셉터 함수들의 타입 정의가 명확하고 안전하게 구현되어 있습니다. AxiosError, AxiosResponse, InternalAxiosRequestConfig 타입을 적절히 활용한 점이 좋습니다.

packages/lib/react-query/src/index.ts (1)

8-8: 내보내기 구문이 일관성 있게 추가되었습니다!

기존 패턴을 따르는 적절한 구현입니다.

apps/web/src/app/[locale]/guild/detail/[id]/page.tsx (1)

3-5: 깔끔한 구현입니다!

Next.js의 페이지 라우팅 컨벤션을 잘 따르고 있으며, 타입 정의도 명확합니다.

apps/web/src/components/GNB/GNB.tsx (1)

7-7: 반응형 구현이 개선되었습니다!

MediaQuery 컴포넌트를 사용하여 조건부 렌더링을 구현한 것이 좋은 접근 방식입니다. 이는 다음과 같은 이점이 있습니다:

  • 불필요한 렌더링 방지
  • 더 깔끔한 코드 구조
  • 유지보수성 향상
packages/api/src/schema.ts (2)

7-13: 페이지네이션 스키마가 잘 구현되었습니다!

필수 필드들이 모두 포함되어 있고, nextPageprevPage의 nullable 처리가 적절합니다.


15-15: 타입 추론 방식이 적절합니다.

z.infer를 사용한 타입 추론이 타입 안정성을 보장하는 좋은 방식입니다.

packages/api/src/index.ts (1)

3-3: 모듈 내보내기가 일관성 있게 구현되었습니다.

기존 패턴을 따라 schema와 guild 모듈을 적절히 내보내고 있습니다.

Also applies to: 12-12

packages/api/src/guild/getGuildIcons.ts (1)

13-17: API 응답 처리가 깔끔합니다.

safeRenderGet을 사용한 안전한 API 호출과 응답 처리가 잘 구현되어 있습니다.

packages/api/src/guild/getAllJoinGuilds.ts (2)

1-2: 문서화가 잘 되어있습니다.

한글로 API의 기능을 명확하게 설명하고 있어 좋습니다.


7-15: 스키마 재사용과 구현이 깔끔합니다.

외부 GuildSchema를 재사용하여 중복을 피하고, 응답 타입과 API 호출이 깔끔하게 구현되어 있습니다.

packages/api/src/guild/getGuildBackgrounds.ts (1)

4-9: 스키마 정의가 명확합니다.

BackgroundSchemaGetGuildBackgroundsResponseSchema의 정의가 명확하고 일관성 있게 구현되어 있습니다.

apps/web/src/hooks/useGetNewUrl.tsx (1)

8-18: URL 파라미터 관리를 위한 훌륭한 커스텀 훅 구현입니다!

코드가 깔끔하고 재사용성이 높으며, React hooks의 모범 사례를 잘 따르고 있습니다.

apps/web/src/app/[locale]/guild/(components)/GuidlInfoForm.tsx (2)

13-24: 주석 처리된 코드의 구현이 필요합니다.

주석으로 처리된 코드에는 다음과 같은 중요한 기능들이 포함되어 있습니다:

  • 폼 상태 관리 (useFormState)
  • GuildInfoFormClient 렌더링
  • 상태 메시지 표시
  • 제출 버튼

이러한 기능들의 구현이 완료되어야 합니다.


3-13: ⚠️ Potential issue

구현이 완료되지 않은 컴포넌트입니다.

현재 빈 프래그먼트만 반환하고 있습니다. 주석 처리된 코드를 보면 form 관련 기능이 계획되어 있는 것으로 보입니다.

다음 사항들을 확인해주세요:

  1. 폼 상태 관리 구현
  2. 폼 제출 핸들러 구현
  3. GuildInfoFormClient 컴포넌트 연동
apps/web/src/app/[locale]/guild/[id]/setting/page.tsx (1)

8-17: 🛠️ Refactor suggestion

API 호출에 대한 오류 처리가 필요합니다.

여러 API 호출이 연속적으로 실행되고 있지만, 오류 처리가 되어있지 않습니다. 사용자 경험 향상을 위해 오류 처리를 추가하는 것이 좋겠습니다.

export default async function GuildSettingPage({ params }: { params: { id: string } }) {
+  try {
    const isLeader = await checkIsLeader(params.id);
    const icons = await getGuildIcons();
    const backgrounds = await getGuildBackgrounds();
    const data = await getGuildById({ guildId: params.id });

    if (!isLeader) {
      redirect(`/guild/${params.id}`);
    }
+  } catch (error) {
+    console.error('길드 설정 페이지 로딩 중 오류 발생:', error);
+    redirect(`/guild/${params.id}`);
+  }
✅ Verification successful

API 호출에 대한 오류 처리 추가가 필요합니다

코드베이스의 다른 부분들과의 일관성을 위해 try-catch 블록을 추가하는 것이 좋습니다. 오류 발생 시 사용자를 길드 페이지로 리다이렉트하여 graceful하게 처리하는 것을 제안드립니다.

export default async function GuildSettingPage({ params }: { params: { id: string } }) {
+  try {
    const isLeader = await checkIsLeader(params.id);
    const icons = await getGuildIcons();
    const backgrounds = await getGuildBackgrounds();
    const data = await getGuildById({ guildId: params.id });

    if (!isLeader) {
      redirect(`/guild/${params.id}`);
    }
+  } catch (error) {
+    console.error('길드 설정 페이지 로딩 중 오류 발생:', error);
+    redirect(`/guild/${params.id}`);
+  }
🏁 Scripts executed

The following scripts were executed for the analysis:

Error: No shell scripts found


Script:

#!/bin/bash
# Check implementation of API functions
ast-grep --pattern 'export $_ function checkIsLeader($_) { $$$ }'
ast-grep --pattern 'export $_ function getGuildIcons($_) { $$$ }'
ast-grep --pattern 'export $_ function getGuildBackgrounds($_) { $$$ }'
ast-grep --pattern 'export $_ function getGuildById($_) { $$$ }'

# Look for error handling patterns in similar components
ast-grep --pattern 'try { $$$ } catch (error) { $$$ }'

# Check for error boundary components
fd -e tsx -e ts ErrorBoundary

Length of output: 20822

apps/web/src/app/[locale]/error.tsx (1)

37-37: 로그아웃 로직 제거에 대한 검토 필요

에러 발생 시 사용자 세션을 초기화하는 로직이 제거되었습니다. 이로 인해 인증 관련 에러가 발생했을 때 사용자가 수동으로 로그아웃해야 할 수 있습니다.

apps/web/src/components/Gitanimals.tsx (1)

18-18: URL 변경이 일관되게 적용되었습니다!

모든 컴포넌트에서 URL이 통일성 있게 변경되었습니다.

Also applies to: 37-37, 61-61, 80-80

apps/web/src/app/[locale]/guild/(components)/GuildDetail.tsx (1)

66-68: 미완성된 컴포넌트 처리 필요

"farm type" 텍스트가 있는 박스는 미완성으로 보입니다. 이를 TODO 주석으로 표시하거나 완성된 구현이 필요합니다.

apps/web/src/app/[locale]/guild/(components)/GuildCard.tsx (1)

61-69: 주석 처리된 코드 정리 필요

GuildJoinPetSelectDialog 관련 코드가 주석 처리되어 있습니다. 이 기능의 구현 계획이 있다면 TODO 주석으로 표시하고, 그렇지 않다면 제거하는 것이 좋습니다.

apps/web/src/app/[locale]/guild/page.tsx (1)

1-25: 코드가 깔끔하고 잘 구성되어 있습니다!

필요한 모든 의존성이 잘 임포트되어 있으며, 검색 매개변수에 대한 타입이 명확하게 정의되어 있습니다.

apps/web/package.json (1)

29-29: 의존성 추가가 적절합니다.

Radix UI의 드롭다운 메뉴 컴포넌트는 접근성이 뛰어나고 안정적인 선택입니다.

apps/web/messages/ko_KR.json (1)

11-12: 번역이 자연스럽습니다.

한글 번역이 간단명료하게 잘 되어있습니다.

apps/web/messages/en_US.json (1)

6-6: 번역이 일관성 있게 잘 되어있습니다.

영문 번역이 다른 메뉴 아이템들과 일관된 대문자 스타일을 유지하고 있습니다.

@@ -1,5 +1,6 @@
import type { Metadata } from 'next';
import { setRequestInterceptor, setResponseInterceptor } from '@gitanimals/api';
import { setRenderRequestInterceptor, setRenderResponseInterceptor } from '@gitanimals/api/src/_instance';
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

내부 구현 세부사항 import 개선 필요

'_instance'와 같은 내부 구현 세부사항을 직접 import하는 것은 권장되지 않습니다. 대신 메인 패키지 진입점인 '@gitanimals/api'에서 import하는 것이 좋습니다.

다음과 같이 수정을 제안합니다:

-import { setRenderRequestInterceptor, setRenderResponseInterceptor } from '@gitanimals/api/src/_instance';
+import { setRenderRequestInterceptor, setRenderResponseInterceptor } from '@gitanimals/api';
📝 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
import { setRenderRequestInterceptor, setRenderResponseInterceptor } from '@gitanimals/api/src/_instance';
import { setRenderRequestInterceptor, setRenderResponseInterceptor } from '@gitanimals/api';

Comment on lines 8 to 10
export async function joinGuildAction(request: JoinGuildRequest) {
return joinGuild(request);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

에러 처리 로직 추가가 필요합니다.

API 호출 시 발생할 수 있는 예외 상황에 대한 처리가 없습니다. try-catch 블록을 추가하여 에러를 적절히 처리하는 것이 좋겠습니다.

 export async function joinGuildAction(request: JoinGuildRequest) {
-  return joinGuild(request);
+  try {
+    return await joinGuild(request);
+  } catch (error) {
+    console.error('길드 참가 중 오류 발생:', error);
+    throw new Error('길드 참가에 실패했습니다.');
+  }
 }
📝 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
export async function joinGuildAction(request: JoinGuildRequest) {
return joinGuild(request);
}
export async function joinGuildAction(request: JoinGuildRequest) {
try {
return await joinGuild(request);
} catch (error) {
console.error('길드 참가 중 오류 발생:', error);
throw new Error('길드 참가에 실패했습니다.');
}
}

Comment on lines 12 to 26
// NOTE: 작업 중
export const createGuildAction = async (prevState: FormState, formData: FormData) => {
const name = formData.get('name') as string;
const description = formData.get('description') as string;
const icon = formData.get('icon') as string;
const background = formData.get('background') as string;

if (!name || !description || !icon || !background) {
return { message: 'Please fill in all fields', success: false };
}

await new Promise((resolve) => setTimeout(resolve, 3000));
console.log('form end');
return { message: 'Guild created', success: true };
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

작업 중인 코드의 완성이 필요합니다.

다음 사항들을 개선해주시기 바랍니다:

  1. API 연동이 구현되어 있지 않습니다
  2. 테스트용 setTimeout은 제거가 필요합니다
  3. console.log는 제거가 필요합니다

또한 updateGuildAction과 유사한 에러 처리 패턴을 적용하는 것이 좋겠습니다:

 export const createGuildAction = async (prevState: FormState, formData: FormData) => {
   const name = formData.get('name') as string;
   const description = formData.get('description') as string;
   const icon = formData.get('icon') as string;
   const background = formData.get('background') as string;

   if (!name || !description || !icon || !background) {
     return { message: 'Please fill in all fields', success: false };
   }

-  await new Promise((resolve) => setTimeout(resolve, 3000));
-  console.log('form end');
-  return { message: 'Guild created', success: true };
+  try {
+    await createGuild({
+      title: name,
+      body: description,
+      guildIcon: icon,
+      farmType: background,
+      autoJoin: true,
+    });
+    return { message: '길드가 생성되었습니다', success: true };
+  } catch (error) {
+    console.error('길드 생성 중 오류 발생:', error);
+    return { message: '길드 생성에 실패했습니다', success: false };
+  }
 };

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

Comment on lines 25 to 26
setRenderRequestInterceptor(interceptorRequestFulfilled);
setRenderResponseInterceptor(interceptorResponseFulfilled, interceptorResponseRejected);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

인터셉터 설정의 중앙화 필요

SessionLoader와 동일한 인터셉터 설정 코드가 중복되어 있습니다. 이는 유지보수성을 저하시킬 수 있습니다.

제안사항:

  1. 인터셉터 설정을 중앙화된 위치로 이동
  2. 공통 설정 함수 생성 고려

예시 구현:

// utils/interceptors.ts
export const setupAllInterceptors = () => {
  setRequestInterceptor(interceptorRequestFulfilled);
  setResponseInterceptor(interceptorResponseFulfilled, interceptorResponseRejected);
  setRenderRequestInterceptor(interceptorRequestFulfilled);
  setRenderResponseInterceptor(interceptorResponseFulfilled, interceptorResponseRejected);
};

@@ -1,4 +1,4 @@
import axios from 'axios';
import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { setInterceptors } from '../_interceptor';

const API_URL = 'https://render.gitanimals.org';
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

API URL 환경 설정 개선 필요

API URL이 하드코딩되어 있습니다. 이는 환경별 배포에 제약이 될 수 있습니다.

제안사항:
환경 변수를 사용하여 API URL을 설정하도록 수정해주세요.

-const API_URL = 'https://render.gitanimals.org';
+const API_URL = process.env.RENDER_API_URL || 'https://render.gitanimals.org';
📝 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 API_URL = 'https://render.gitanimals.org';
const API_URL = process.env.RENDER_API_URL || 'https://render.gitanimals.org';

Comment on lines 26 to 27
<img src={data.guildIcon} width={40} height={40} alt={data.title} />
<h2>{data.title}</h2>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성 개선 필요

이미지 태그에 의미 있는 대체 텍스트를 제공해야 합니다. 현재는 title만 사용되고 있습니다.

-<img src={data.guildIcon} width={40} height={40} alt={data.title} />
+<img 
+  src={data.guildIcon} 
+  width={40} 
+  height={40} 
+  alt={`${data.title} 길드 아이콘`} 
+/>
📝 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
<img src={data.guildIcon} width={40} height={40} alt={data.title} />
<h2>{data.title}</h2>
<img
src={data.guildIcon}
width={40}
height={40}
alt={`${data.title} 길드 아이콘`}
/>
<h2>{data.title}</h2>

Comment on lines 23 to 34
const submitJoinGuild = async (selectPersona: string) => {
try {
await joinGuildAction({
guildId: guild.id,
personaId: selectPersona,
});

queryClient.invalidateQueries({ queryKey: inboxQueries.allKey() });
} catch (error) {
console.error(error);
}
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

에러 처리 개선 필요

현재 에러를 단순히 콘솔에 로깅하는 것은 충분하지 않습니다. 사용자에게 적절한 에러 메시지를 표시해야 합니다.

 const submitJoinGuild = async (selectPersona: string) => {
   try {
     await joinGuildAction({
       guildId: guild.id,
       personaId: selectPersona,
     });

     queryClient.invalidateQueries({ queryKey: inboxQueries.allKey() });
   } catch (error) {
-    console.error(error);
+    // TODO: 에러 처리 컴포넌트나 토스트 메시지 추가
+    throw new Error('길드 가입 중 오류가 발생했습니다. 다시 시도해 주세요.');
   }
 };

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

Comment on lines 14 to 16
export interface FormState {
message: string;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

FormState 인터페이스 개선 필요

FormState 인터페이스가 현재 message 필드만 포함하고 있습니다. 폼의 모든 필드를 포함하도록 확장해야 합니다.

 export interface FormState {
-  message: string;
+  name: string;
+  description: string;
+  icon: string;
+  background: string;
 }
📝 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
export interface FormState {
message: string;
}
export interface FormState {
name: string;
description: string;
icon: string;
background: string;
}

Comment on lines 29 to 36
<TextField
name="name"
placeholder="Type guild name"
className={css({ mb: '6px' })}
defaultValue={props.initialData?.title}
/>
<TextArea placeholder="Type guild description" name="description" defaultValue={props.initialData?.body} />
</div>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

폼 유효성 검사 추가 필요

길드 이름과 설명에 대한 유효성 검사가 없습니다. 최소/최대 길이, 필수 입력 여부 등의 검증이 필요합니다.

 <TextField
   name="name"
   placeholder="Type guild name"
   className={css({ mb: '6px' })}
   defaultValue={props.initialData?.title}
+  required
+  minLength={2}
+  maxLength={50}
 />
 <TextArea 
   placeholder="Type guild description" 
   name="description" 
   defaultValue={props.initialData?.body}
+  required
+  maxLength={500}
 />
📝 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
<TextField
name="name"
placeholder="Type guild name"
className={css({ mb: '6px' })}
defaultValue={props.initialData?.title}
/>
<TextArea placeholder="Type guild description" name="description" defaultValue={props.initialData?.body} />
</div>
<TextField
name="name"
placeholder="Type guild name"
className={css({ mb: '6px' })}
defaultValue={props.initialData?.title}
required
minLength={2}
maxLength={50}
/>
<TextArea
placeholder="Type guild description"
name="description"
defaultValue={props.initialData?.body}
required
maxLength={500}
/>
</div>

Comment on lines 38 to 59
<p className={headingStyle}>Guild thumbnail</p>
<SelectableFormItem name="icon" defaultValue={props.initialData?.guildIcon}>
<Flex gap="6px">
{props.icons.map((icon) => (
<SelectableFormItemOption value={icon} key={icon}>
{({ isSelected }) => (
<img
src={icon}
className={cx(
itemStyle,
isSelected && css({ border: '1.5px solid', borderColor: 'white.white_90' }),
)}
width={70}
height={70}
alt={icon}
/>
)}
</SelectableFormItemOption>
))}
</Flex>
</SelectableFormItem>
</div>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성 개선 필요

썸네일 선택 UI에 적절한 레이블과 설명이 없습니다. 스크린 리더 사용자를 위한 접근성 개선이 필요합니다.

-<p className={headingStyle}>Guild thumbnail</p>
+<p className={headingStyle} id="thumbnail-label">길드 썸네일 선택</p>
 <SelectableFormItem 
   name="icon" 
   defaultValue={props.initialData?.guildIcon}
+  aria-labelledby="thumbnail-label"
+  role="radiogroup"
 >
📝 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={headingStyle}>Guild thumbnail</p>
<SelectableFormItem name="icon" defaultValue={props.initialData?.guildIcon}>
<Flex gap="6px">
{props.icons.map((icon) => (
<SelectableFormItemOption value={icon} key={icon}>
{({ isSelected }) => (
<img
src={icon}
className={cx(
itemStyle,
isSelected && css({ border: '1.5px solid', borderColor: 'white.white_90' }),
)}
width={70}
height={70}
alt={icon}
/>
)}
</SelectableFormItemOption>
))}
</Flex>
</SelectableFormItem>
</div>
<p className={headingStyle} id="thumbnail-label">길드 썸네일 선택</p>
<SelectableFormItem
name="icon"
defaultValue={props.initialData?.guildIcon}
aria-labelledby="thumbnail-label"
role="radiogroup"
>
<Flex gap="6px">
{props.icons.map((icon) => (
<SelectableFormItemOption value={icon} key={icon}>
{({ isSelected }) => (
<img
src={icon}
className={cx(
itemStyle,
isSelected && css({ border: '1.5px solid', borderColor: 'white.white_90' }),
)}
width={70}
height={70}
alt={icon}
/>
)}
</SelectableFormItemOption>
))}
</Flex>
</SelectableFormItem>
</div>

@sumi-0011 sumi-0011 changed the base branch from main to feat/guild-update-modal January 10, 2025 14:48
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.

Actionable comments posted: 24

🧹 Nitpick comments (16)
apps/web/src/app/[locale]/error.tsx (1)

Line range hint 1-57: 전반적인 에러 처리 구조가 잘 설계되어 있습니다

다음과 같은 긍정적인 부분들이 있습니다:

  1. 개발 환경과 프로덕션 환경의 구분
  2. 알려진 에러 메시지에 대한 별도 처리
  3. Slack을 통한 에러 모니터링
  4. 사용자 친화적인 에러 메시지와 복구 옵션 제공

한 가지 제안사항으로는, 알려진 에러 메시지(KNOWN_ERROR_MESSAGES)를 상수 파일로 분리하여 관리하면 더 좋을 것 같습니다.

-const KNOWN_ERROR_MESSAGES = [NOT_AUTHORIZED_MESSAGE];

이 부분을 별도의 상수 파일로 이동하는 것을 고려해보시겠습니까?

apps/web/src/app/[locale]/guild/[id]/setting/page.tsx (2)

9-9: 타입 정의를 개선하세요

props 타입을 인라인으로 정의하는 대신 별도의 인터페이스로 분리하면 코드의 재사용성과 유지보수성이 향상됩니다.

+interface GuildSettingPageProps {
+  params: {
+    id: string;
+  };
+}
+
-export default async function GuildSettingPage({ params }: { params: { id: string } }) {
+export default async function GuildSettingPage({ params }: GuildSettingPageProps) {

16-18: 권한 체크 중 로딩 상태 표시가 필요합니다

사용자 경험 향상을 위해 권한 체크 중임을 나타내는 로딩 상태를 추가하는 것이 좋습니다.

권한 체크 중 로딩 컴포넌트를 표시하도록 수정하시겠습니까? 필요하시다면 구현을 도와드릴 수 있습니다.

apps/web/src/components/Trigger/BackTrigger.tsx (1)

11-14: 접근성 및 타입 안전성 개선이 필요합니다.

다음 사항들을 개선하면 좋을 것 같습니다:

  1. 버튼에 적절한 aria-label 추가
  2. children prop이 없을 경우의 기본 텍스트 설정
  3. onClick 이벤트 핸들러 분리
-export function BackTrigger(props: ComponentProps<'button'>) {
+type BackTriggerProps = ComponentProps<'button'> & {
+  ariaLabel?: string;
+};
+
+export function BackTrigger({ 
+  children = '뒤로 가기',
+  ariaLabel = '이전 페이지로 이동',
+  ...props 
+}: BackTriggerProps) {
   const router = useRouter();
+  
+  const handleClick = () => router.back();
 
-  return <button onClick={() => router.back()} {...props} />;
+  return (
+    <button 
+      onClick={handleClick}
+      aria-label={ariaLabel}
+      {...props}
+    >
+      {children}
+    </button>
+  );
 }
apps/web/src/app/[locale]/guild/_components/GuildModalPageLayout.tsx (1)

35-37: 타이틀 컴포넌트의 유연성 개선이 필요합니다.

타이틀 컴포넌트에 대한 개선사항입니다:

  1. 헤딩 레벨 커스터마이징 지원
  2. 스타일 확장성 개선
-export const GuildModalPageTitle = ({ children, className }: PropsWithChildren<{ className?: string }>) => (
+type GuildModalPageTitleProps = PropsWithChildren<{
+  className?: string;
+  as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
+}>;
+
+export const GuildModalPageTitle = ({ 
+  children, 
+  className,
+  as: Heading = 'h2'
+}: GuildModalPageTitleProps) => (
-  <h2 className={cx(dialogTitleStyle, className)}>{children}</h2>
+  <Heading className={cx(dialogTitleStyle, className)}>{children}</Heading>
 );
apps/web/src/app/[locale]/guild/_components/GuildDetail.tsx (2)

1-15: 이미지 처리 방식 개선 필요

Next.js의 Image 컴포넌트를 사용하지 않는 이유가 명확하지 않습니다. Next/Image를 사용하면 다음과 같은 이점이 있습니다:

  • 자동 이미지 최적화
  • 레이아웃 시프트 방지
  • 더 나은 로딩 성능
- /* eslint-disable @next/next/no-img-element */
- import { UsersRoundIcon } from 'lucide-react';
+ import Image from 'next/image';
+ import { UsersRoundIcon } from 'lucide-react';

22-30: 접근성 개선 필요

헤더 섹션의 접근성을 개선할 수 있습니다:

  • 시맨틱 HTML 사용
  • ARIA 레이블 추가
  • 이미지 로드 실패 처리
- <div>
+ <header role="banner">
-   <div className={titleStyle}>
+   <div className={titleStyle} aria-label="길드 정보">
      <img 
        src={data.guildIcon} 
        width={40} 
        height={40} 
        alt={data.title}
+       onError={(e) => {
+         e.currentTarget.src = '/default-guild-icon.png';
+       }}
      />
-     <h2>{data.title}</h2>
+     <h2 aria-label="길드 이름">{data.title}</h2>
    </div>
-   <div className={bodyStyle}>{data.body}</div>
+   <div className={bodyStyle} aria-label="길드 설명">{data.body}</div>
- </div>
+ </header>
apps/web/src/app/[locale]/guild/_components/GuildPetSelectDialog.tsx (2)

10-21: 콜백 처리 개선이 필요합니다.

현재 구현에서 다음과 같은 개선사항을 제안드립니다:

  1. onSubmit 콜백 실행 시 발생할 수 있는 에러 처리가 필요합니다
  2. 제출 중 로딩 상태 관리가 필요합니다

다음과 같이 개선해보세요:

 export const GuildJoinPetSelectDialog = ({ onSubmit }: { onSubmit: (selectPersona: string) => void }) => {
   const [selectPersona, setSelectPersona] = useState<string>();
+  const [isSubmitting, setIsSubmitting] = useState(false);
 
   const onSelectPersona = (currentSelectPersona: Persona) => {
     setSelectPersona(currentSelectPersona.id);
   };
 
-  const onDone = () => {
+  const onDone = async () => {
     if (selectPersona) {
-      onSubmit(selectPersona);
+      try {
+        setIsSubmitting(true);
+        await onSubmit(selectPersona);
+      } catch (error) {
+        console.error('Failed to submit selected persona:', error);
+        // TODO: 에러 처리 UI 추가
+      } finally {
+        setIsSubmitting(false);
+      }
     }
   };

23-30: UI 및 접근성 개선이 필요합니다.

다음과 같은 개선사항을 제안드립니다:

  1. 버튼의 비활성화 상태에 로딩 상태도 반영해야 합니다
  2. 접근성 향상을 위한 aria- 속성 추가가 필요합니다
   return (
     <>
       <SelectPersonaList selectPersona={selectPersona ? [selectPersona] : []} onSelectPersona={onSelectPersona} />
-      <Button mx="auto" w="100px" onClick={onDone} disabled={!selectPersona}>
-        Done
+      <Button 
+        mx="auto" 
+        w="100px" 
+        onClick={onDone} 
+        disabled={!selectPersona || isSubmitting}
+        aria-busy={isSubmitting}
+      >
+        {isSubmitting ? 'Submitting...' : 'Done'}
       </Button>
     </>
   );
apps/web/src/app/[locale]/guild/_components/GuildSearch.tsx (1)

31-43: 접근성 및 사용자 경험 개선이 필요합니다.

리셋 버튼과 검색바에 대한 개선사항입니다:

  1. 리셋 버튼에 접근성 레이블 추가
  2. 검색 중 로딩 상태 표시
  3. 검색바 비활성화 상태 관리
   return (
     <>
       {text && (
-        <button onClick={onSearchReset}>
+        <button 
+          onClick={onSearchReset}
+          aria-label="검색 초기화"
+          disabled={isSearching}
+        >
           <ChevronLeftIcon size="28px" color="#FFFFFF80" />
         </button>
       )}
 
       <Box flex="1">
-        <SearchBar defaultValue={text} onChange={(e) => setInput(e.target.value)} onSubmit={onSubmit} />
+        <SearchBar 
+          value={input}
+          onChange={(e) => setInput(e.target.value)}
+          onSubmit={onSubmit}
+          disabled={isSearching}
+          placeholder={isSearching ? "검색 중..." : "길드 검색"}
+        />
       </Box>
     </>
   );
apps/web/src/app/[locale]/guild/_components/SortSelect.tsx (1)

10-16: 상수 분리 및 타입 안정성 개선이 필요합니다.

정렬 옵션 배열을 다음과 같이 개선하면 좋을 것 같습니다:

  1. 상수를 별도 파일로 분리
  2. 타입 안정성 강화

src/constants/guild.ts 파일을 생성하고 다음과 같이 구현해보세요:

import type { FilterType } from '@gitanimals/api';

export interface SortOption {
  label: string;
  value: FilterType;
}

export const GUILD_SORT_OPTIONS: readonly SortOption[] = [
  { label: 'Random', value: 'RANDOM' },
  { label: 'People (asc)', value: 'PEOPLE_ASC' },
  { label: 'People (desc)', value: 'PEOPLE_DESC' },
  { label: 'Contribution (asc)', value: 'CONTRIBUTION_ASC' },
  { label: 'Contribution (desc)', value: 'CONTRIBUTION_DESC' },
] as const;
apps/web/src/app/[locale]/guild/_components/SelecableFormItem.tsx (1)

50-54: 이벤트 핸들러 타입 정의가 필요합니다.

SelectableFormItemOptionProps 인터페이스에 이벤트 핸들러 관련 타입이 누락되어 있습니다.

다음과 같이 수정하는 것을 제안합니다:

 interface SelectableFormItemOptionProps {
   className?: string;
   children: React.ReactNode | ((props: { isSelected: boolean }) => React.ReactNode);
   value: string;
+  onSelect?: (value: string) => void;
+  onFocus?: React.FocusEventHandler<HTMLButtonElement>;
+  onBlur?: React.FocusEventHandler<HTMLButtonElement>;
 }
apps/web/src/app/[locale]/guild/_components/SelectPersonaList.tsx (2)

51-51: 정렬 기능 구현이 필요합니다.

TODO 주석으로 표시된 정렬 기능이 구현되지 않았습니다. 페르소나 목록을 레벨이나 이름순으로 정렬하는 기능을 추가하는 것이 좋습니다.

정렬 기능 구현을 도와드릴까요? GitHub 이슈를 생성하여 추적할 수 있습니다.


96-98: 메모이제이션 최적화가 필요합니다.

현재 메모이제이션 조건이 제한적입니다. 페르소나의 다른 속성이 변경될 때도 불필요한 리렌더링이 발생할 수 있습니다.

 const MemoizedPersonaItem = memo(PersonaItem, (prev, next) => {
-  return prev.isSelected === next.isSelected && prev.persona.level === next.persona.level;
+  return (
+    prev.isSelected === next.isSelected &&
+    prev.persona.level === next.persona.level &&
+    prev.persona.id === next.persona.id &&
+    prev.persona.type === next.persona.type
+  );
 });
apps/web/src/app/[locale]/guild/_components/GuildCreateForm.tsx (1)

12-15: 타입 안정성 개선이 필요합니다

formData의 키 값들이 'title', 'body', 'icon', 'background' 등으로 한정되어 있으므로, Record 타입 대신 구체적인 타입을 사용하는 것이 좋습니다.

export interface GuildCreateFormProps {
-  formData: Record<string, string>;
+  formData: {
+    title?: string;
+    body?: string;
+    icon?: string;
+    background?: string;
+  };
  onDataChange: (key: string, value: string) => void;
}
apps/web/src/app/[locale]/guild/page.tsx (1)

99-108: 반응형 디자인 개선이 필요합니다

카드 리스트의 반응형 처리가 모바일에서 최적화되어 있지 않습니다.

  • 모바일에서 단일 컬럼으로 변경하는 대신 그리드 시스템 활용
  • 하드코딩된 크기값들을 테마 시스템으로 관리
const cardListStyle = grid({
  gridTemplateRows: 'repeat(3, 210px)',
-  columns: 3,
+  gridTemplateColumns: {
+    base: '1fr',
+    sm: 'repeat(2, 1fr)',
+    md: 'repeat(3, 1fr)',
+  },
  gap: '8px',
  w: 'full',
-  _mobile: {
-    columns: 1,
-  },
});
🛑 Comments failed to post (24)
apps/web/src/app/[locale]/guild/[id]/setting/page.tsx (2)

22-22: 🛠️ Refactor suggestion

제목 텍스트에 국제화(i18n)를 적용해주세요

하드코딩된 영문 텍스트 "Guild Setting"을 국제화하여 다국어 지원이 가능하도록 수정해주세요.

-      <GuildModalPageTitle>Guild Setting</GuildModalPageTitle>
+      <GuildModalPageTitle>{t('guild.setting')}</GuildModalPageTitle>

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


10-14: 🛠️ Refactor suggestion

데이터 페칭 최적화 및 에러 처리가 필요합니다

  1. Promise.all을 사용하여 병렬로 데이터를 페칭하면 성능이 향상됩니다.
  2. API 호출 실패에 대한 에러 처리가 없습니다.
-  const isLeader = await checkIsLeader(params.id);
-  const icons = await getGuildIcons();
-  const backgrounds = await getGuildBackgrounds();
-  const data = await getGuildById({ guildId: params.id });
+  try {
+    const [isLeader, icons, backgrounds, data] = await Promise.all([
+      checkIsLeader(params.id),
+      getGuildIcons(),
+      getGuildBackgrounds(),
+      getGuildById({ guildId: params.id })
+    ]);
+  } catch (error) {
+    // TODO: 에러 처리 로직 추가
+    console.error('길드 설정 데이터 로딩 실패:', error);
+    throw error;
+  }

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

apps/web/src/app/[locale]/guild/_components/GuildModalPageLayout.tsx (1)

9-33: 🛠️ Refactor suggestion

시맨틱 마크업 및 접근성 개선이 필요합니다.

현재 구현에서 다음과 같은 개선사항들이 있습니다:

  1. 시맨틱 HTML 요소 사용 (main, article 등)
  2. ARIA 속성 추가
  3. 스타일 속성의 재사용성 개선
+const modalLayoutStyles = {
+  wrapper: css({ 
+    minH: '100%', 
+    display: 'flex', 
+    justifyContent: 'center', 
+    alignItems: 'center' 
+  }),
+  container: css({
+    maxWidth: '880px',
+    mx: 'auto',
+    background: 'gray.gray_150',
+    p: '40px',
+    borderRadius: '16px',
+    color: 'white.white',
+    w: '100%',
+    position: 'relative',
+  })
+};

-export function GuildModalPageLayout({ children }: PropsWithChildren) {
+type GuildModalPageLayoutProps = PropsWithChildren<{
+  ariaLabel?: string;
+}>;
+
+export function GuildModalPageLayout({ 
+  children, 
+  ariaLabel = '길드 설정', 
+}: GuildModalPageLayoutProps) {
   return (
-    <div className={css({ minH: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' })}>
-      <div
-        className={css({
-          maxWidth: '880px',
-          mx: 'auto',
-          background: 'gray.gray_150',
-          p: '40px',
-          borderRadius: '16px',
-          color: 'white.white',
-          w: '100%',
-          position: 'relative',
-        })}
+    <main className={modalLayoutStyles.wrapper}>
+      <article
+        role="dialog"
+        aria-label={ariaLabel}
+        className={modalLayoutStyles.container}
       >
         <BackTrigger className={css({ position: 'absolute', top: '16px', right: '16px' })}>
           <XIcon />
         </BackTrigger>
         <Flex flexDirection="column" gap="24px">
           {children}
         </Flex>
-      </div>
-    </div>
+      </article>
+    </main>
   );
 }
📝 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.

const modalLayoutStyles = {
  wrapper: css({ 
    minH: '100%', 
    display: 'flex', 
    justifyContent: 'center', 
    alignItems: 'center' 
  }),
  container: css({
    maxWidth: '880px',
    mx: 'auto',
    background: 'gray.gray_150',
    p: '40px',
    borderRadius: '16px',
    color: 'white.white',
    w: '100%',
    position: 'relative',
  })
};

type GuildModalPageLayoutProps = PropsWithChildren<{
  ariaLabel?: string;
}>;

export function GuildModalPageLayout({ 
  children, 
  ariaLabel = '길드 설정', 
}: GuildModalPageLayoutProps) {
  return (
    <main className={modalLayoutStyles.wrapper}>
      <article
        role="dialog"
        aria-label={ariaLabel}
        className={modalLayoutStyles.container}
      >
        <BackTrigger className={css({ position: 'absolute', top: '16px', right: '16px' })}>
          <XIcon />
        </BackTrigger>
        <Flex flexDirection="column" gap="24px">
          {children}
        </Flex>
      </article>
    </main>
  );
}
apps/web/src/app/[locale]/guild/_components/GuildDetail.tsx (3)

31-40: 🛠️ Refactor suggestion

국제화(i18n) 적용 필요

문자열이 하드코딩되어 있어 다국어 지원이 어렵습니다. 국제화를 위해 번역 키를 사용하는 것이 좋습니다.

  <div className={leaderStyle}>
-   <p>Leader</p>
+   <p>{t('guild.leader')}</p>
    <BannerPetSelectMedium
      name={data.leader.name}
      count={data.leader.contributions}
      image={getPersonaImage(data.leader.personaType)}
      status="gradient"
+     aria-label={t('guild.leader_info', { name: data.leader.name })}
    />
  </div>

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


16-21: 🛠️ Refactor suggestion

에러 처리 및 로딩 상태 개선 필요

현재 Suspense와 ErrorBoundary의 fallback이 null로 설정되어 있어 사용자 경험이 좋지 않을 수 있습니다. 적절한 로딩 상태와 에러 메시지를 표시하는 것이 좋습니다.

export const GuildDetail = wrap
-  .Suspense({ fallback: null })
-  .ErrorBoundary({ fallback: null })
+  .Suspense({ fallback: <LoadingSpinner /> })
+  .ErrorBoundary({ 
+    fallback: ({ error }) => (
+      <ErrorMessage 
+        message="길드 정보를 불러오는 중 오류가 발생했습니다."
+        error={error} 
+      />
+    )
+  })

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


41-64: 🛠️ Refactor suggestion

성능 최적화 및 코드 개선 필요

다음과 같은 개선이 필요합니다:

  1. 멤버 제한이 하드코딩되어 있습니다
  2. key prop이 중복 사용되고 있습니다
  3. 큰 리스트에 대한 성능 최적화가 필요합니다
+ const MEMBER_LIMIT = 15;
  <Flex mb="1" justifyContent="space-between">
-   <p>Members</p>
+   <p>{t('guild.members')}</p>
    <Flex gap="6px" alignItems="center">
      <UsersRoundIcon size={16} color="#FFFFFF80" />
-     <span>{data.members.length}/ 15</span>
+     <span>{data.members.length}/ {MEMBER_LIMIT}</span>
    </Flex>
  </Flex>
  <Flicking 
    moveType="freeScroll" 
    align="prev" 
    bound={true}
+   virtualSize={data.members.length}
+   renderOnlyVisible={true}
  >
    {data.members.map((member) => (
      <div
        className={cx('flicking-panel', css({ height: 'fit-content', _first: { ml: 0 }, marginLeft: 1 }))}
        key={member.id}
      >
        <BannerPetSelectMedium
-         key={member.id}
          name={member.name}
          count={member.contributions}
          image={member.personaId}
+         aria-label={t('guild.member_info', { name: member.name })}
        />
      </div>
    ))}
  </Flicking>
📝 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.

          <div className={membersStyle}>
            <Flex mb="1" justifyContent="space-between">
              <p>{t('guild.members')}</p>
              <Flex gap="6px" alignItems="center">
                <UsersRoundIcon size={16} color="#FFFFFF80" />
                <span>{data.members.length}/ {MEMBER_LIMIT}</span>
              </Flex>
            </Flex>
            <Flicking 
              moveType="freeScroll" 
              align="prev" 
              bound={true}
              virtualSize={data.members.length}
              renderOnlyVisible={true}
            >
              {data.members.map((member) => (
                <div
                  className={cx('flicking-panel', css({ height: 'fit-content', _first: { ml: 0 }, marginLeft: 1 }))}
                  key={member.id}
                >
                  <BannerPetSelectMedium
                    name={member.name}
                    count={member.contributions}
                    image={member.personaId}
                    aria-label={t('guild.member_info', { name: member.name })}
                  />
                </div>
              ))}
            </Flicking>
          </div>
apps/web/src/app/[locale]/guild/_components/GuildSearch.tsx (1)

17-24: 🛠️ Refactor suggestion

검색 기능 최적화가 필요합니다.

현재 구현에서 다음과 같은 개선사항이 필요합니다:

  1. 검색 입력 시 디바운스 처리가 필요합니다
  2. URL과 입력 상태의 동기화가 필요합니다
  3. 검색 중 로딩 상태 표시가 필요합니다
+  const [isSearching, setIsSearching] = useState(false);
-  const [input, setInput] = useState('');
+  const [input, setInput] = useState(text);
+  
+  useEffect(() => {
+    setInput(text);
+  }, [text]);
+
+  const debouncedSearch = useCallback(
+    debounce(async (value: string) => {
+      setIsSearching(true);
+      const nextUrl = getNextUrl({ text: value });
+      await router.replace(nextUrl);
+      setIsSearching(false);
+    }, 300),
+    [getNextUrl, router]
+  );
 
-  const onSubmit = () => {
-    const nextUrl = getNextUrl({ text: input });
-    router.replace(nextUrl);
+  const onSubmit = async () => {
+    await debouncedSearch(input);
   };

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

apps/web/src/app/[locale]/guild/_components/SortSelect.tsx (1)

18-43: 🛠️ Refactor suggestion

상태 관리 및 에러 처리 개선이 필요합니다.

다음과 같은 개선사항을 제안드립니다:

  1. 정렬 변경 시 로딩 상태 관리
  2. URL 업데이트 실패 시 에러 처리
  3. 컴포넌트 비활성화 상태 관리
 export function SortSelect() {
   const router = useRouter();
   const getNextUrl = useGetNextUrl();
   const searchParams = useSearchParams();
   const filter = (searchParams.get('filter') as FilterType) ?? 'RANDOM';
+  const [isUpdating, setIsUpdating] = useState(false);
 
-  const onSelect = (value: FilterType) => {
-    const nextUrl = getNextUrl({ filter: value });
-    router.replace(nextUrl);
+  const onSelect = async (value: FilterType) => {
+    try {
+      setIsUpdating(true);
+      const nextUrl = getNextUrl({ filter: value });
+      await router.replace(nextUrl);
+    } catch (error) {
+      console.error('Failed to update sort filter:', error);
+      // TODO: 에러 처리 UI 추가
+    } finally {
+      setIsUpdating(false);
+    }
   };
 
   return (
-    <CombineChip defaultValue={filter} onValueChange={(value) => onSelect(value as FilterType)}>
+    <CombineChip 
+      defaultValue={filter} 
+      onValueChange={(value) => onSelect(value as FilterType)}
+      disabled={isUpdating}
+    >
       <CombineChip.Trigger>
-        <CombineChip.Value />
+        <CombineChip.Value />
+        {isUpdating && <span aria-label="정렬 중...">⌛</span>}
       </CombineChip.Trigger>
📝 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.

export function SortSelect() {
  const router = useRouter();
  const getNextUrl = useGetNextUrl();
  const searchParams = useSearchParams();
  const filter = (searchParams.get('filter') as FilterType) ?? 'RANDOM';
  const [isUpdating, setIsUpdating] = useState(false);

  const onSelect = async (value: FilterType) => {
    try {
      setIsUpdating(true);
      const nextUrl = getNextUrl({ filter: value });
      await router.replace(nextUrl);
    } catch (error) {
      console.error('Failed to update sort filter:', error);
      // TODO: 에러 처리 UI 추가
    } finally {
      setIsUpdating(false);
    }
  };

  return (
    <CombineChip 
      defaultValue={filter} 
      onValueChange={(value) => onSelect(value as FilterType)}
      disabled={isUpdating}
    >
      <CombineChip.Trigger>
        <CombineChip.Value />
        {isUpdating && <span aria-label="정렬 중...">⌛</span>}
      </CombineChip.Trigger>
      <CombineChip.Content>
        {options.map((option) => (
          <CombineChip.Item key={option.value} value={option.value}>
            {option.label}
          </CombineChip.Item>
        ))}
      </CombineChip.Content>
    </CombineChip>
  );
}
apps/web/src/app/[locale]/guild/_components/GuildCreate.tsx (2)

14-21: 🛠️ Refactor suggestion

폼 데이터 유효성 검사가 필요합니다.

현재 빈 문자열 체크만 수행하고 있습니다. 길드 제목 길이 제한이나 특수문자 제한 등의 추가적인 유효성 검사가 필요합니다.

다음과 같은 유효성 검사 함수를 추가하는 것을 제안합니다:

+ const validateFormData = (data: typeof formData) => {
+   const errors: Record<string, string> = {};
+   if (data.title.length < 2 || data.title.length > 20) {
+     errors.title = '길드 이름은 2-20자 사이여야 합니다';
+   }
+   if (data.body.length > 200) {
+     errors.body = '길드 설명은 200자를 초과할 수 없습니다';
+   }
+   return errors;
+ };

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


29-41: ⚠️ Potential issue

에러 처리 로직이 누락되었습니다.

에러가 발생했을 때 사용자에게 적절한 피드백을 제공하지 않고 있습니다. 에러 처리를 추가하고 로딩 상태를 표시하는 것이 좋습니다.

다음과 같이 수정하는 것을 제안합니다:

  const onSubmit = async () => {
+   const [isLoading, setIsLoading] = useState(false);
    try {
+     setIsLoading(true);
      const res = await createGuild({
        title: formData.title,
        body: formData.body,
        guildIcon: formData.icon,
        autoJoin: false,
        farmType: formData.background,
        personaId: selectPersona,
      });
      router.push(`/guild/${res.id}`);
-   } catch (error) {}
+   } catch (error) {
+     console.error('길드 생성 중 오류 발생:', error);
+     // TODO: 사용자에게 에러 메시지 표시
+   } finally {
+     setIsLoading(false);
+   }
  };

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

apps/web/src/app/[locale]/guild/_components/SelecableFormItem.tsx (1)

65-72: 🛠️ Refactor suggestion

접근성 개선이 필요합니다.

버튼 요소에 접근성 속성이 누락되어 있습니다. 스크린 리더 사용자를 위한 ARIA 속성을 추가해야 합니다.

다음과 같이 수정하는 것을 제안합니다:

    <button
      type="button"
      className={cx('selectable-form-item-option group', props.className)}
      onClick={() => onChange(props.value)}
      data-active={selected === props.value}
+     role="radio"
+     aria-checked={selected === props.value}
+     aria-label={`${props.value} 선택 옵션`}
    >
      {typeof props.children === 'function' ? props.children({ isSelected: selected === props.value }) : props.children}
    </button>
📝 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.

    <button
      type="button"
      className={cx('selectable-form-item-option group', props.className)}
      onClick={() => onChange(props.value)}
      data-active={selected === props.value}
      role="radio"
      aria-checked={selected === props.value}
      aria-label={`${props.value} 선택 옵션`}
    >
      {typeof props.children === 'function' ? props.children({ isSelected: selected === props.value }) : props.children}
    </button>
apps/web/src/app/[locale]/guild/_components/SelectPersonaList.tsx (1)

38-40: 🛠️ Refactor suggestion

에러 메시지 현지화가 필요합니다.

ErrorBoundary의 fallback에 하드코딩된 에러 메시지가 있습니다. 다국어 지원을 위해 현지화가 필요합니다.

 export const SelectPersonaList = wrap
-  .ErrorBoundary({ fallback: <div>error</div> })
+  .ErrorBoundary({ 
+    fallback: <div>{t('common.error.failed_to_load_personas')}</div> 
+  })

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

apps/web/src/app/[locale]/guild/_components/GuildCard.tsx (3)

23-34: 🛠️ Refactor suggestion

길드 가입 시 로딩 상태 처리가 필요합니다.

submitJoinGuild 함수에서 로딩 상태 처리가 누락되어 있습니다. 사용자에게 적절한 피드백을 제공해야 합니다.

+ const [isJoining, setIsJoining] = useState(false);
  const submitJoinGuild = async (selectPersona: string) => {
    try {
+     setIsJoining(true);
      await joinGuildAction({
        guildId: guild.id,
        personaId: selectPersona,
      });

      queryClient.invalidateQueries({ queryKey: inboxQueries.allKey() });
    } catch (error) {
      console.error(error);
+     // TODO: 에러 메시지 표시
+   } finally {
+     setIsJoining(false);
    }
  };
📝 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.

  const [isJoining, setIsJoining] = useState(false);
  const submitJoinGuild = async (selectPersona: string) => {
    try {
      setIsJoining(true);
      await joinGuildAction({
        guildId: guild.id,
        personaId: selectPersona,
      });

      queryClient.invalidateQueries({ queryKey: inboxQueries.allKey() });
    } catch (error) {
      console.error(error);
      // TODO: 에러 메시지 표시
    } finally {
      setIsJoining(false);
    }
  };

43-43: 🛠️ Refactor suggestion

하드코딩된 멤버 제한값이 있습니다.

멤버 제한이 15로 하드코딩되어 있습니다. 이는 설정 가능한 값이어야 합니다.

- <span>{guild.members.length}/ 15</span>
+ <span>{guild.members.length} / {guild.memberLimit}</span>

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


61-69: 💡 Codebase verification

주석 처리된 코드를 제거해 주세요

GuildJoinPetSelectDialog 컴포넌트는 이미 GuildPetSelectDialog.tsx에 구현되어 있고 다른 파일들에서 정상적으로 사용되고 있습니다. 따라서 GuildCard.tsx의 주석 처리된 코드는 불필요한 중복이므로 제거하는 것이 좋겠습니다.

🔗 Analysis chain

주석 처리된 코드를 정리해야 합니다.

구현되지 않은 기능에 대한 주석 처리된 코드가 있습니다. 이는 제거하거나 구현을 완료해야 합니다.

주석 처리된 GuildJoinPetSelectDialog 컴포넌트의 구현 상태를 확인하겠습니다:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# GuildJoinPetSelectDialog 컴포넌트 구현 여부 확인
rg -l "GuildJoinPetSelectDialog"

Length of output: 338


Script:

#!/bin/bash
# Check the implementation of both dialog components
echo "=== GuildPetSelectDialog.tsx ==="
cat apps/web/src/app/[locale]/guild/_components/GuildPetSelectDialog.tsx

echo -e "\n=== Looking for GuildJoinPetSelectDialog implementation ==="
rg "export.*GuildJoinPetSelectDialog" -A 5

Length of output: 1919

apps/web/src/app/[locale]/guild/_components/GuidlInfoFormClient.tsx (2)

44-53: 🛠️ Refactor suggestion

접근성 및 이미지 대체 텍스트 개선 필요

이미지의 alt 텍스트가 파일 이름이나 background 값을 그대로 사용하고 있어 스크린 리더 사용자에게 의미 있는 정보를 제공하지 못합니다.

 <img
   src={icon}
   className={cx(
     itemStyle,
     isSelected && css({ border: '1.5px solid', borderColor: 'white.white_90' }),
   )}
   width={70}
   height={70}
-  alt={icon}
+  alt={`길드 아이콘 옵션: ${icon.split('/').pop()?.split('.')[0]}`}
 />

 <img
   src={getBackgroundImage(background)}
   className={cx(
     itemStyle,
     isSelected &&
       css({
         border: '1.5px solid',
         borderColor: 'white.white_90',
       }),
   )}
   width={284}
   height={124}
   key={background}
-  alt={background}
+  alt={`길드 배경 옵션: ${background}`}
 />

Also applies to: 67-81


29-36: 🛠️ Refactor suggestion

폼 유효성 검사 개선 필요

현재 필드의 기본적인 유효성 검사가 누락되어 있습니다. 길드 이름과 설명에 대한 최소/최대 길이 제한이나 특수문자 제한 등의 유효성 검사가 필요합니다.

 <TextField
   name="name"
   placeholder="Type guild name"
   className={css({ mb: '6px' })}
   defaultValue={props.initialData?.title}
+  required
+  minLength={2}
+  maxLength={50}
 />
 <TextArea 
   placeholder="Type guild description" 
   name="description" 
   defaultValue={props.initialData?.body}
+  required
+  minLength={10}
+  maxLength={500}
 />
📝 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.

        <TextField
          name="name"
          placeholder="Type guild name"
          className={css({ mb: '6px' })}
          defaultValue={props.initialData?.title}
          required
          minLength={2}
          maxLength={50}
        />
        <TextArea 
          placeholder="Type guild description" 
          name="description" 
          defaultValue={props.initialData?.body}
          required
          minLength={10}
          maxLength={500}
        />
      </div>
apps/web/src/app/[locale]/guild/@modal/(.)detail/[id]/join/page.tsx (1)

43-43: 🛠️ Refactor suggestion

빈 스타일 객체 수정 필요

dialogTitleStyle이 빈 객체로 정의되어 있습니다. 필요한 스타일을 추가하거나 불필요한 경우 제거해주세요.

-const dialogTitleStyle = css({});
+const dialogTitleStyle = css({
+  textStyle: 'glyph24.bold',
+  textAlign: 'center',
+  mb: 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.

const dialogTitleStyle = css({
  textStyle: 'glyph24.bold',
  textAlign: 'center',
  mb: 2
});
apps/web/src/serverActions/guild.ts (1)

51-53: 🛠️ Refactor suggestion

로깅 개선 필요

에러 로깅이 중복되어 있으며, 운영 환경에 적합하지 않습니다. 구조화된 로깅으로 개선이 필요합니다.

   } catch (error) {
-    console.log('error: ', error);
-    console.log('error: ', (error as Error).message);
+    console.error('길드 수정 실패:', {
+      guildId,
+      error: error instanceof Error ? error.message : String(error)
+    });
     return { message: 'Failed to update guild', success: false };
   }
📝 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.

    console.error('길드 수정 실패:', {
      guildId,
      error: error instanceof Error ? error.message : String(error)
    });
    return { message: 'Failed to update guild', success: false };
apps/web/src/app/[locale]/guild/_components/GuildCreateForm.tsx (3)

24-32: 🛠️ Refactor suggestion

필수 입력 필드 표시와 유효성 검사가 필요합니다

길드 이름과 설명이 필수 입력 항목인지 사용자가 알 수 없습니다. 또한 입력값에 대한 유효성 검사가 없습니다.

  • 필수 입력 필드에 대한 표시 추가
  • 길드 이름 길이 제한 추가
  • 설명 글자 수 제한 추가

36-50: 🛠️ Refactor suggestion

⚠️ Potential issue

코드 중복을 제거하고 이미지 선택 컴포넌트를 분리해주세요

아이콘과 배경 이미지 선택 부분의 코드가 매우 유사합니다. 재사용 가능한 컴포넌트로 분리하는 것이 좋습니다.

또한 img 태그에 불필요한 key prop이 중복 사용되고 있습니다.

- <img
-   src={icon}
-   className={itemStyle}
-   width={70}
-   height={70}
-   key={icon}
-   alt={icon}
-   style={{
-     border: formData.icon === icon ? '1.5px solid' : 'none',
-   }}
- />
+ <img
+   src={icon}
+   className={itemStyle}
+   width={70}
+   height={70}
+   alt={icon}
+   style={{
+     border: formData.icon === icon ? '1.5px solid' : 'none',
+   }}
+ />

Also applies to: 56-70


18-19: 🛠️ Refactor suggestion

접근성 개선과 로딩 상태 처리가 필요합니다

데이터 로딩 중일 때의 상태 처리가 없으며, 버튼들에 대한 접근성 레이블이 누락되었습니다.

  • isLoading 상태에 따른 로딩 UI 추가
  • 버튼에 aria-label 추가
  • 선택된 아이템에 대한 aria-selected 상태 추가
apps/web/src/app/[locale]/guild/page.tsx (2)

27-36: ⚠️ Potential issue

주석 처리된 코드를 정리하고 라우팅 로직을 명확히 해주세요

불필요한 주석 처리된 코드가 있으며, allJoinGuilds 결과가 사용되지 않고 있습니다. 라우팅 로직의 의도가 불분명합니다.

  • 주석 처리된 코드 제거
  • allJoinGuilds 관련 로직 정리
  • 라우팅 의도에 대한 주석 추가

38-50: 🛠️ Refactor suggestion

URL 생성 함수를 분리하고 에러 처리를 추가해주세요

URL 생성 로직이 컴포넌트 내부에 인라인으로 구현되어 있으며, API 호출에 대한 에러 처리가 없습니다.

  • URL 생성 함수를 별도의 유틸리티로 분리
  • searchGuild API 호출에 대한 에러 처리 추가
  • randomId 파싱 실패에 대한 예외 처리 추가

Base automatically changed from feat/guild-update-modal to guild January 11, 2025 06:48
# Conflicts:
#	apps/web/src/app/[locale]/guild/(components)/GuildSearch.tsx
#	apps/web/src/app/[locale]/guild/(components)/SelectPersonaList.tsx
#	apps/web/src/app/[locale]/guild/(components)/SortSelect.tsx
#	apps/web/src/app/[locale]/guild/(list)/GuildPetSelectDialog.tsx
#	apps/web/src/app/[locale]/guild/@modal/(.)create/page.tsx
#	apps/web/src/app/[locale]/guild/@modal/(.)detail/[id]/join/page.tsx
#	apps/web/src/app/[locale]/guild/@modal/(.)detail/[id]/page.tsx
#	apps/web/src/app/[locale]/guild/GuildSearch.tsx
#	apps/web/src/app/[locale]/guild/SortSelect.tsx
#	apps/web/src/app/[locale]/guild/[id]/MoreMenu.tsx
#	apps/web/src/app/[locale]/guild/[id]/setting/GuildSetting.tsx
#	apps/web/src/app/[locale]/guild/[id]/setting/layout.tsx
#	apps/web/src/app/[locale]/guild/[id]/setting/page.tsx
#	apps/web/src/app/[locale]/guild/_components/GuildSearch.tsx
#	apps/web/src/app/[locale]/guild/_components/SelectPersonaList.tsx
#	apps/web/src/app/[locale]/guild/_components/SortSelect.tsx
#	apps/web/src/app/[locale]/guild/create/page.tsx
#	apps/web/src/app/[locale]/guild/detail/[id]/join/page.tsx
#	apps/web/src/app/[locale]/guild/detail/[id]/layout.tsx
#	apps/web/src/app/[locale]/guild/detail/[id]/page.tsx
#	apps/web/src/app/[locale]/guild/page.tsx
#	apps/web/src/serverActions/guild.ts
#	packages/api/src/guild/index.ts
@sumi-0011 sumi-0011 merged commit 22d6d97 into guild Jan 11, 2025
1 check passed
@sumi-0011 sumi-0011 deleted the feat/guild-member branch January 11, 2025 06:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant