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

[실험실] 같은 레벨, 타입의 펫 한번에 팔기 기능 #248

Merged
merged 4 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion apps/web/messages/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@
"merge-result": "Merge result",
"cancel": "Cancel",
"please-choose-pet": "Please choose a pet to use to merge the level. The pet used disappears."
}
},
"laboratory": "Laboratory"
},
"Event": {
"event-end": "Event has ended.",
Expand All @@ -127,5 +128,12 @@
"not-login-toast": "🎃 It's Halloween! 🎃\nLogin and draw your Halloween pet!",
"go": "GO!"
}
},
"Laboratory": {
"property-pet-sell": {
"title": "Property Pet Sell",
"description": "Sell your pet to other users!",
"count": "ea"
}
}
}
10 changes: 9 additions & 1 deletion apps/web/messages/ko_KR.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@
"merge-result": "합치기 결과",
"cancel": "취소",
"please-choose-pet": "합치기에 사용할 펫을 선택해주세요. 합친 펫은 사라집니다."
}
},
"laboratory": "실험실"
},
"Event": {
"event-end": "이벤트가 종료되었습니다.",
Expand All @@ -129,5 +130,12 @@
"not-login-toast": "🎃 Halloween이 찾아왔어요! 🎃\n로그인 후 할로윈 펫을 뽑아보세요!",
"go": "GO!"
}
},
"Laboratory": {
"property-pet-sell": {
"title": "Property Pet Sell",
"description": "Sell your pet to other users!",
"count": "개"
Comment on lines +136 to +138
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

영문 텍스트의 한글화가 필요합니다.

Laboratory 섹션의 텍스트가 한글로 번역되지 않았습니다.

다음과 같이 수정해주세요:

  "Laboratory": {
    "property-pet-sell": {
-     "title": "Property Pet Sell",
-     "description": "Sell your pet to other users!",
+     "title": "펫 일괄 판매",
+     "description": "다른 사용자에게 펫을 판매하세요!",
      "count": "개"
    }
  }

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

}
}
}
36 changes: 36 additions & 0 deletions apps/web/src/app/[locale]/laboratory/_component/AlertDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import { css } from '_panda/css';
import { Flex } from '_panda/jsx';
import { Button, Dialog } from '@gitanimals/ui-panda';

export function AlertDialog(props: {
isOpen: boolean;
onClose: () => void;
title: string;
description: React.ReactNode;
}) {
return (
<Dialog open={props.isOpen} onOpenChange={props.onClose}>
<Dialog.Content>
<Dialog.Title className={titleStyle}>{props.title}</Dialog.Title>
<Dialog.Description className={descriptionStyle}>{props.description}</Dialog.Description>
<Flex gap="8px" justifyContent="flex-end" width="100%">
<Button onClick={props.onClose} variant="primary" size="m">
Close
</Button>
</Flex>
</Dialog.Content>
</Dialog>
);
}
const titleStyle = css({
textStyle: 'glyph20.regular',
textAlign: 'left',
});

const descriptionStyle = css({
textStyle: 'glyph16.regular',
textAlign: 'left',
color: 'white.white_75',
width: '100%',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { css } from '_panda/css';
import { Flex } from '_panda/jsx';
import { Button, Dialog } from '@gitanimals/ui-panda';

export function ConfirmDialog(props: {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
description: string;
}) {
return (
<Dialog open={props.isOpen} onOpenChange={props.onClose}>
<Dialog.Content>
<Dialog.Title className={titleStyle}>{props.title}</Dialog.Title>
<Dialog.Description className={descriptionStyle}>{props.description}</Dialog.Description>
<Flex gap="8px" justifyContent="flex-end" width="100%">
<Button onClick={props.onConfirm} variant="secondary" size="m">
Ok
</Button>
<Button onClick={props.onClose} variant="primary" size="m">
Close
</Button>
Comment on lines +18 to +23
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

버튼 텍스트 현지화 및 UX 개선이 필요합니다

  1. "Ok"와 "Close" 텍스트가 하드코딩되어 있습니다
  2. 확인/취소 버튼의 순서가 일반적인 UX 패턴과 다릅니다
  3. variant 설정이 직관적이지 않습니다 (취소는 secondary, 확인은 primary가 일반적)
-          <Button onClick={props.onConfirm} variant="secondary" size="m">
-            Ok
-          </Button>
-          <Button onClick={props.onClose} variant="primary" size="m">
-            Close
+          <Button onClick={props.onClose} variant="secondary" size="m">
+            취소
+          </Button>
+          <Button onClick={props.onConfirm} variant="primary" size="m">
+            확인
           </Button>

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

</Flex>
</Dialog.Content>
</Dialog>
);
}
const titleStyle = css({
textStyle: 'glyph20.regular',
textAlign: 'left',
});

const descriptionStyle = css({
textStyle: 'glyph16.regular',
textAlign: 'left',
color: 'white.white_75',
width: '100%',
});
16 changes: 16 additions & 0 deletions apps/web/src/app/[locale]/laboratory/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { css } from '_panda/css';

export default function LaboratoryLayout({ children }: { children: React.ReactNode }) {
return <div className={containerStyle}>{children}</div>;
}
const containerStyle = css({
minHeight: '100vh',
height: 'fit-content',
backgroundColor: '#019C5A',
padding: '120px 200px',
color: 'white.white_100',

'@media (max-width: 1400px)': {
padding: '120px 100px',
},
});
56 changes: 56 additions & 0 deletions apps/web/src/app/[locale]/laboratory/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { css } from '_panda/css';

import { Link } from '@/i18n/routing';

export default function LaboratoryPage() {
return (
<div className={contentStyle}>
<Card href="/laboratory/property-pet-sell">
<h2>레벨, 타입 같은 펫 한번에 팔기</h2>
<p>펫 레벨, 타입 등 펫 속성을 선택하여 한번에 팔 수 있어요.</p>
</Card>
Comment on lines +8 to +11
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

텍스트 현지화가 필요합니다

한글로 하드코딩된 텍스트를 현지화해야 합니다. 또한 접근성을 위한 aria-label 추가가 필요합니다.

-      <Card href="/laboratory/property-pet-sell">
-        <h2>레벨, 타입 같은 펫 한번에 팔기</h2>
-        <p>펫 레벨, 타입 등 펫 속성을 선택하여 한번에 팔 수 있어요.</p>
+      <Card 
+        href="/laboratory/property-pet-sell"
+        aria-label={t('Laboratory.property-pet-sell.aria-label')}
+      >
+        <h2>{t('Laboratory.property-pet-sell.title')}</h2>
+        <p>{t('Laboratory.property-pet-sell.description')}</p>
       </Card>

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

<Card href="#">Card</Card>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

미완성된 카드 컴포넌트가 있습니다

개발 중인 카드 컴포넌트가 있습니다. 프로덕션 배포 전에 완성하거나 제거해야 합니다.

-      <Card href="#">Card</Card>

</div>
);
}

function Card({ children, href }: { children: React.ReactNode; href: string }) {
return (
<Link href={href} className={cardStyle}>
{children}
</Link>
);
}

const contentStyle = css({
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '16px',
});

const cardStyle = css({
background: 'white.white_10',
backdropFilter: 'blur(7px)',
borderRadius: '16px',
p: 8,
display: 'flex',
gap: '10px',
textStyle: 'glyph16.regular',
color: 'white.white_100',
flexDirection: 'column',

'& h2 ': {
textStyle: 'glyph24.bold',
},

'& p': {
textStyle: 'glyph16.regular',
},

_hover: {
background: 'white.white_20',
transform: 'translateY(-2px)',
boxShadow: '0 5px 10px rgba(0, 0, 0, 0.1)',
transition: 'background 0.3s ease, transform 0.3s ease',
},
});
165 changes: 165 additions & 0 deletions apps/web/src/app/[locale]/laboratory/property-pet-sell/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
'use client';

import { memo, useMemo } from 'react';
import { useTranslations } from 'next-intl';
import { css, cx } from '_panda/css';
import { dropPets } from '@gitanimals/api/src/shop/dropPet';
import { userQueries } from '@gitanimals/react-query/src/user';
import { LevelBanner } from '@gitanimals/ui-panda';
import { wrap } from '@suspensive/react';
import { useSuspenseQuery } from '@tanstack/react-query';

import { useDialog } from '@/components/GlobalComponent/useDialog';
import { trackEvent } from '@/lib/analytics';
import { customScrollStyle } from '@/styles/scrollStyle';
import { useClientUser } from '@/utils/clientAuth';
import { getPersonaImage } from '@/utils/image';

export default function PropertyPetSellPage() {
return (
<div>
<PersonaList />
</div>
);
}

interface PetItemType {
ids: string[];
type: string;
level: string;
}

const PersonaList = wrap
.ErrorBoundary({ fallback: <div> </div> })
.Suspense({ fallback: <></> })
.on(function PersonaList() {
const { name } = useClientUser();
const { data } = useSuspenseQuery(userQueries.allPersonasOptions(name));

const { showDialog } = useDialog();

const onPetClick = (ids: string[]) => {
showDialog({
title: '펫 판매',
description: `펫을 판매하시겠습니까? ${ids.length}마리를 판매합니다.`,
onConfirm: () => onSell(ids),
});
Comment on lines +43 to +46
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

하드코딩된 문자열을 국제화 처리해주세요

현재 showDialog에서 사용되는 '펫 판매', '펫 판매 완료' 등의 문자열이 하드코딩되어 있습니다. 국제화를 위해 useTranslations를 사용하여 메시지를 처리하는 것이 좋습니다.

Also applies to: 58-68

};

const onSell = async (ids: string[]) => {
const res = await dropPets({ personaIds: ids });

const totalPrice = res.success.reduce((acc, curr) => acc + curr.givenPoint, 0);

trackEvent('laboratory', {
type: '레벨, 타입 같은 펫 한번에 팔기',
});

showDialog({
title: '펫 판매 완료',
description: (
<div>
<p>
{res.success.length}마리 판매 완료, {res.errors.length}마리 판매 실패
</p>
<p>총 판매 금액: {totalPrice}P</p>
</div>
),
});
};
Comment on lines +49 to +69
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

API 호출 오류 처리를 추가해주세요

dropPets API 호출 시 예기치 않은 오류에 대비하여 try-catch 문을 사용하여 오류를 처리하는 것이 좋습니다. 오류 발생 시 사용자에게 적절한 메시지를 표시해주세요.

다음과 같이 코드를 수정해 보세요:

 const onSell = async (ids: string[]) => {
+  try {
     const res = await dropPets({ personaIds: ids });
     // ... 기존 코드 유지
+  } catch (error) {
+    // 오류 메시지 표시
+    showDialog({
+      title: '오류 발생',
+      description: '펫 판매 중 오류가 발생했습니다. 다시 시도해주세요.',
+    });
+    console.error(error);
+  }
 };

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


// 레벨 오름차순 정렬
const petList = useMemo(() => {
const petItemMap = new Map<string, PetItemType>();

data.personas.forEach((persona) => {
const level = persona.level;
const type = persona.type;
const uniqueKey = `${type}-${level}`;
const petItem = petItemMap.get(uniqueKey);
if (petItem) {
petItem.ids.push(persona.id);
} else {
petItemMap.set(uniqueKey, { ids: [persona.id], type, level });
}
});

return Array.from(petItemMap.values()).sort((a, b) => Number(a.level) - Number(b.level));
}, [data.personas]);

return (
<section className={sectionStyle}>
<div className={flexOverflowStyle}>
{petList.map((petItem) => (
<div key={petItem.type + petItem.level}>
<MemoizedPersonaItem
type={petItem.type}
level={petItem.level}
onClick={() => onPetClick(petItem.ids)}
count={petItem.ids.length}
/>
</div>
))}
</div>
</section>
);
});

const sectionStyle = css({
height: '100%',
maxHeight: '100%',
minHeight: '0',
display: 'flex',
flexDirection: 'column',
gap: '16px',
});

const flexOverflowStyle = cx(
css({
display: 'flex',
overflowY: 'auto',
overflowX: 'hidden',
width: '100%',
gap: '12px 4px',
height: '100%',
minHeight: '0',
flexWrap: 'wrap',
maxHeight: 'calc(100% - 24px)',
}),
customScrollStyle,
);

interface PersonaItemProps {
type: string;
level: string;
onClick: () => void;
count: number;
}

function PersonaItem({ type, level, onClick, count }: PersonaItemProps) {
const t = useTranslations('Laboratory.property-pet-sell');
return (
<button onClick={onClick} className={css({ outline: 'none', bg: 'transparent' })}>
<div className={levelTagStyle}>
{count}
{t('count')}
</div>
<LevelBanner image={getPersonaImage(type)} level={Number(level)} size="small" />
</button>
);
}

const MemoizedPersonaItem = memo(PersonaItem, (prev, next) => {
return prev.type === next.type && prev.level === next.level;
});
Comment on lines +139 to +154
Copy link
Member

Choose a reason for hiding this comment

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

요건 인라인으로 붙여도 될 거 같은데, 따로 하신 이유가 모에용?? 궁금

Copy link
Member Author

Choose a reason for hiding this comment

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

메모를 나눠돈 이유를 말하시는걸까요?!
종종 memo 할떄랑 안할때 나눠서 테스ㅌ하기도 하고, 메모된 컴포넌트랑 기존 컴포넌트를 구분 & 따로 두고 싶었습니다.
여기선 큰 상관없지만, 이전에 마이페이지 개발할 때 작업했던것을 따왔어요


const levelTagStyle = css({
borderRadius: '4px',
background: 'black.black_25',
padding: '0 8px',
color: 'white.white_75',
textStyle: 'glyph16.bold',
fontSize: '10px',
lineHeight: '20px',
mb: '4px',
});
Loading
Loading