-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 가게 상세 페이지 태그 추가, 응원 카드 변경 #156
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
Changes from all commits
e63e2ab
1ef3495
8f694ee
145ab6c
23f247e
d7a1479
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,9 @@ | |
|
|
||
| import { Suspense } from "@suspensive/react"; | ||
| import { useSuspenseQuery } from "@tanstack/react-query"; | ||
| import Link from "next/link"; | ||
| import { slice } from "es-toolkit/compat"; | ||
| import { motion } from "motion/react"; | ||
| import Image from "next/image"; | ||
| import { useState } from "react"; | ||
|
|
||
| import { storeCheersQueryOptions } from "@/app/(store)/_api/shop"; | ||
|
|
@@ -11,10 +13,13 @@ import { Button } from "@/components/ui/Button"; | |
| import { Skeleton } from "@/components/ui/Skeleton"; | ||
| import { Spacer } from "@/components/ui/Spacer"; | ||
| import { HStack, VStack } from "@/components/ui/Stack"; | ||
| import { Tag } from "@/components/ui/Tag"; | ||
| import { Text } from "@/components/ui/Text"; | ||
| import { TextButton } from "@/components/ui/TextButton"; | ||
| import { ALL_TAGS } from "@/constants/tag.constants"; | ||
|
|
||
| import * as styles from "./StoreCheers.css"; | ||
| import { getContentBackgroundColor, getHeaderBackgroundColor } from "./utils"; | ||
|
|
||
| export const StoreCheers = ({ storeId }: { storeId: number }) => { | ||
| return ( | ||
|
|
@@ -32,17 +37,21 @@ export const StoreCheers = ({ storeId }: { storeId: number }) => { | |
|
|
||
| const CHEERS_SIZE = 50; | ||
|
|
||
| const ITEMS_PER_PAGE = 3; | ||
|
|
||
| const THEMES = ["yellow", "pink", "blue"] as const; | ||
| type Theme = (typeof THEMES)[number]; | ||
|
|
||
| const CheerContent = ({ storeId }: { storeId: number }) => { | ||
| const { | ||
| data: { cheers }, | ||
| } = useSuspenseQuery(storeCheersQueryOptions(storeId, CHEERS_SIZE)); | ||
|
|
||
| const [visibleCount, setVisibleCount] = useState(3); | ||
|
|
||
| const ITEMS_PER_PAGE = 3; | ||
| const totalItems = cheers.length; | ||
| const isAllVisible = visibleCount >= totalItems; | ||
| const shouldShowToggleButton = totalItems > ITEMS_PER_PAGE; | ||
| const showToggleButton = totalItems > ITEMS_PER_PAGE; | ||
|
|
||
| const handleToggle = () => { | ||
| if (isAllVisible) { | ||
|
|
@@ -59,16 +68,17 @@ const CheerContent = ({ storeId }: { storeId: number }) => { | |
| return ( | ||
| <VStack> | ||
| <VStack gap={24}> | ||
| {visibleCards.map(card => ( | ||
| {visibleCards.map((card, index) => ( | ||
| <CheerCard | ||
| key={card.id} | ||
| author={card.memberNickname} | ||
| content={card.description} | ||
| memberId={card.memberId} | ||
| tags={card.tags} | ||
| theme={THEMES[index % THEMES.length] as Theme} | ||
| /> | ||
| ))} | ||
| </VStack> | ||
| {shouldShowToggleButton && ( | ||
| {showToggleButton && ( | ||
| <> | ||
| <Spacer size={20} /> | ||
| <Button | ||
|
|
@@ -82,64 +92,114 @@ const CheerContent = ({ storeId }: { storeId: number }) => { | |
| </> | ||
| )} | ||
|
|
||
| <Spacer size={shouldShowToggleButton ? 12 : 20} /> | ||
|
|
||
| <Link href={`/stores/register?storeId=${storeId}`}> | ||
| <Button variant='primary' size='large' fullWidth> | ||
| 가게 응원하기 | ||
| </Button> | ||
| </Link> | ||
| <Spacer size={showToggleButton ? 12 : 20} /> | ||
| </VStack> | ||
| ); | ||
| }; | ||
|
|
||
| const CheerCard = ({ | ||
| author, | ||
| content, | ||
| memberId, | ||
| tags, | ||
| theme, | ||
| }: { | ||
| author: string; | ||
| content: string; | ||
| memberId: number; | ||
| tags: string[]; | ||
| theme: Theme; | ||
| }) => { | ||
| const [isExpanded, setIsExpanded] = useState(false); | ||
|
|
||
| const isLongText = content.length > 50; | ||
|
|
||
| const selectedTags = ALL_TAGS.filter(tag => tags.includes(tag.name)); | ||
| const visibleTags = isExpanded ? selectedTags : slice(selectedTags, 0, 2); | ||
| const additionalTagsCount = Math.max( | ||
| 0, | ||
| selectedTags.length - visibleTags.length | ||
| ); | ||
| const showAdditionalTags = additionalTagsCount > 0 && !isExpanded; | ||
|
|
||
| return ( | ||
| <VStack gap={12}> | ||
| <HStack align='center' gap={8}> | ||
| <Avatar memberId={memberId} className={styles.cheerCardProfileImage} /> | ||
| <motion.div | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. motion까쥐 🤩🤩 |
||
| className={styles.cheerCard} | ||
| onClick={() => setIsExpanded(!isExpanded)} | ||
| role='button' | ||
| tabIndex={0} | ||
| transition={{ duration: 0.3 }} | ||
| whileTap={{ scale: 0.99 }} | ||
| > | ||
| <div | ||
| className={styles.cheerCardHeader} | ||
| style={{ | ||
| backgroundColor: getHeaderBackgroundColor(theme), | ||
| }} | ||
| > | ||
| <Avatar | ||
| // TODO: 추후 theme 지정 가능하게끔 수정 | ||
| memberId={THEMES.findIndex(t => t === theme)} | ||
| className={styles.cheerCardAvatar} | ||
| /> | ||
| <Text as='span' typo='body1Sb' color='text.normal'> | ||
| {author} | ||
| </Text> | ||
| </HStack> | ||
|
|
||
| <HStack align='stretch'> | ||
| <hr className={styles.cheerCardDivider} /> | ||
| <VStack className={styles.cheerCardContent} gap={4} align='start'> | ||
| <Text | ||
| as='p' | ||
| typo='body2Rg' | ||
| color='text.normal' | ||
| className={styles.cheerCardContentText} | ||
| data-expanded={isExpanded} | ||
| data-long-text={isLongText} | ||
| > | ||
| {content} | ||
| </Text> | ||
| {isLongText && ( | ||
| <TextButton | ||
| size='small' | ||
| variant='assistive' | ||
| onClick={() => setIsExpanded(!isExpanded)} | ||
| </div> | ||
|
|
||
| <div | ||
| className={styles.cheerCardContent} | ||
| style={{ | ||
| backgroundColor: getContentBackgroundColor(theme), | ||
| }} | ||
| > | ||
| <VStack gap={8} align='start'> | ||
| <VStack gap={4} align='start'> | ||
| <Text | ||
| as='p' | ||
| typo='body2Rg' | ||
| color='text.normal' | ||
| className={styles.cheerCardContentText} | ||
| data-expanded={isExpanded} | ||
| data-long-text={isLongText} | ||
| > | ||
| {isExpanded ? "접기" : "더보기"} | ||
| </TextButton> | ||
| )} | ||
| {content} | ||
| </Text> | ||
| {isLongText && ( | ||
| <TextButton | ||
| size='small' | ||
| variant='assistive' | ||
| onClick={() => setIsExpanded(!isExpanded)} | ||
| > | ||
| {isExpanded ? "접기" : "더보기"} | ||
| </TextButton> | ||
| )} | ||
|
Comment on lines
+166
to
+173
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 카드 전체 onClick과 “더보기/접기” 버튼 onClick이 중복 호출되어 토글이 무력화됨 부모 - <motion.div
+ <motion.div
className={styles.cheerCard}
- onClick={() => setIsExpanded(!isExpanded)}
+ onClick={() => setIsExpanded(prev => !prev)}
role='button'
tabIndex={0}
+ aria-expanded={isExpanded}
+ onKeyDown={e => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ setIsExpanded(prev => !prev);
+ }
+ }}
transition={{ duration: 0.3 }}
whileTap={{ scale: 0.99 }}
>
@@
- <TextButton
+ <TextButton
size='small'
variant='assistive'
- onClick={() => setIsExpanded(!isExpanded)}
+ onClick={e => {
+ e.stopPropagation();
+ setIsExpanded(prev => !prev);
+ }}
>Also applies to: 123-131 🤖 Prompt for AI Agents |
||
| </VStack> | ||
|
|
||
| <HStack gap={8} align='start' wrap='wrap'> | ||
| {visibleTags.map(tag => ( | ||
| <Tag key={tag.name}> | ||
| <Image | ||
| src={tag.iconUrl} | ||
| alt={tag.label} | ||
| width={16} | ||
| height={16} | ||
| className={styles.tagIcon} | ||
| /> | ||
| <Text as='span' typo='caption1Sb' color='text.primary'> | ||
| {tag.label} | ||
| </Text> | ||
| </Tag> | ||
| ))} | ||
|
|
||
| {showAdditionalTags && ( | ||
| <Tag> | ||
| <Text as='span' typo='caption1Sb' color='text.primary'> | ||
| +{additionalTagsCount} | ||
| </Text> | ||
| </Tag> | ||
| )} | ||
| </HStack> | ||
| </VStack> | ||
| </HStack> | ||
| </VStack> | ||
| </div> | ||
| </motion.div> | ||
| ); | ||
| }; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export type Theme = "yellow" | "pink" | "blue"; |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 배경 색상 지정하는 함수 꽤나 쓰이는거 같은데, 이것도 분리해서 함께 사용하면 좋을 거 같네유 ~ . ~ |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,32 @@ | ||||||||||||||||||
| import { type Theme } from "./types"; | ||||||||||||||||||
|
|
||||||||||||||||||
| export const getCheerCardTheme = (memberId: number) => { | ||||||||||||||||||
| return ["yellow", "pink", "blue"][memberId % 3]; | ||||||||||||||||||
| }; | ||||||||||||||||||
|
Comment on lines
+3
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Theme 반환값이
-export const getCheerCardTheme = (memberId: number) => {
- return ["yellow", "pink", "blue"][memberId % 3];
-};
+export const getCheerCardTheme = (memberId: number): Theme => {
+ const themes: Theme[] = ["yellow", "pink", "blue"];
+ const safeIndex = Math.abs(Math.trunc(memberId)) % themes.length;
+ return themes[safeIndex];
+};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents💡 Verification agent ❓ Verification inconclusive유틸이 실제로 사용되는지 확인 필요 이번 PR에서 다음 스크립트로 사용 여부를 확인할 수 있습니다. 🏁 Script executed: #!/bin/bash
# getCheerCardTheme 사용처 검색
rg -nP '\bgetCheerCardTheme\s*\(' -S --type ts --type tsx -C2Length of output: 91
🏁 Script executed: #!/bin/bash
# getCheerCardTheme 사용처 검색 (glob)
rg -nP '\bgetCheerCardTheme\s*\(' -n --glob '*.ts' --glob '*.tsx' -C2Length of output: 71
현재
대상 위치 🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| // 색상 테마별 배경색 헬퍼 함수 | ||||||||||||||||||
| export const getHeaderBackgroundColor = (colorName: Theme) => { | ||||||||||||||||||
| switch (colorName) { | ||||||||||||||||||
| case "yellow": | ||||||||||||||||||
| return "#fceb9c"; | ||||||||||||||||||
| case "pink": | ||||||||||||||||||
| return "#fabdb8"; | ||||||||||||||||||
| case "blue": | ||||||||||||||||||
| return "#b2dfff"; | ||||||||||||||||||
| default: | ||||||||||||||||||
| return "#fceb9c"; | ||||||||||||||||||
| } | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| export const getContentBackgroundColor = (colorName: Theme) => { | ||||||||||||||||||
| switch (colorName) { | ||||||||||||||||||
| case "yellow": | ||||||||||||||||||
| return "#fef8dd"; | ||||||||||||||||||
| case "pink": | ||||||||||||||||||
| return "#fde5e3"; | ||||||||||||||||||
| case "blue": | ||||||||||||||||||
| return "#e0f2ff"; | ||||||||||||||||||
| default: | ||||||||||||||||||
| return "#fef8dd"; | ||||||||||||||||||
| } | ||||||||||||||||||
| }; | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3:
옹! 뭔가 사용안하는 스타일인거 같아서 제거해도 될 거 같은 느낌!