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

feat: 톡픽 상세 조회 모바일 컴포넌트 구현 #307

Open
wants to merge 16 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
9a9f88b
design: Button 컴포넌트에 outlineHighLight 스타일 값 추가
areumH Feb 16, 2025
f13b0f2
feat: Button 컴포넌트 스토리북 텍스트 수정
areumH Feb 16, 2025
adda972
design: SummaryItem 컴포넌트 구현
areumH Feb 16, 2025
facdd05
feat: SummaryBox 컴포넌트 구현 및 스토리북 작성
areumH Feb 16, 2025
0e6efe4
feat: VoteToggle 컴포넌트 구현 및 스토리북 작성
areumH Feb 16, 2025
a2330c1
refactor: TalkPickSection 웹 컴포넌트 스토리북 수정
areumH Feb 17, 2025
6ad78aa
feat: TalkPickSection 모바일 컴포넌트 구현 및 스토리북 작성
areumH Feb 17, 2025
38b2c52
refactor: ProfileIcon 컴포넌트에 사이즈 값 추가 및 TalkPickSection 모바일 컴포넌트 스타일 수정
areumH Feb 18, 2025
c9e5967
feat: ReportModal 모바일 컴포넌트 구현 및 스토리북 작성
areumH Feb 18, 2025
071ce13
refactor: ReportModal 모바일 컴포넌트 커서 스타일 추가 및 사유 선택 로직 수정
areumH Feb 18, 2025
3029212
feat: 톡픽 상단 텍스트 수정 및 신고 모달 로직 연결
areumH Feb 21, 2025
25ea527
feat: 톡픽에 작성자 프로필 이미지가 보여지도록 연결
areumH Feb 21, 2025
a6b25ed
refactor: ProfileIcon 이미지 크기 수정 및 톡픽 컴포넌트 스타일 수정
areumH Feb 24, 2025
300456e
refactor: 프로필 아이콘에 데이터를 옵셔널로 전달 및 format 함수에 기본값 연결 수정
areumH Feb 24, 2025
00705ac
refactor: 토스트 모달 스타일 수정
areumH Feb 24, 2025
b64bfb2
refactor: ReportModal, TalkPickSection 이미지에 key 속성 추가
areumH Feb 24, 2025
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
2 changes: 2 additions & 0 deletions src/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ export { default as LogoMedium } from './svg/logo-medium.svg';
export { default as DefaultPerson } from './svg/default-person.svg';
export { default as MobileDefaultPerson } from './svg/mobile-default-person.svg';
export { default as MobilePlus } from './svg/mobile-plus.svg';
export { default as MobileReport } from './svg/mobile-report.svg';
export { default as PickIcon } from './svg/pick-icon.svg';

// TODO: 이전 SVG
export { default as Email } from './svg/email.svg';
Expand Down
2 changes: 1 addition & 1 deletion src/assets/svg/angle-small-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/svg/angle-small-up.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/assets/svg/mobile-report.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/svg/pick-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 31 additions & 14 deletions src/components/atoms/ProfileIcon/ProfileIcon.style.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
import { css } from '@emotion/react';

export const profileWrapper = (size: 'small' | 'large') =>
css({
all: 'unset',
width: size === 'small' ? '40px' : '142px',
height: size === 'small' ? '40px' : '142px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
overflow: 'hidden',
backgroundSize: 'cover',
backgroundPosition: 'center',
cursor: 'pointer',
});
export const profileWrapper = css({
all: 'unset',
display: 'flex',
flexShrink: 0,
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
overflow: 'hidden',
backgroundSize: 'cover',
backgroundPosition: 'center',
cursor: 'pointer',
});

export const getProfileSize = (size: 'extraSmall' | 'small' | 'large') => {
const style = {
large: css({
width: '142px',
height: '142px',
}),
small: css({
width: '40px',
height: '40px',
}),
extraSmall: css({
width: '24px',
height: '24px',
}),
};

return style[size];
};

export const profileImage = css({
width: '100%',
Expand Down
24 changes: 19 additions & 5 deletions src/components/atoms/ProfileIcon/ProfileIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import React, { ComponentPropsWithRef, ForwardedRef, forwardRef } from 'react';
import { NormalProfile } from '@/assets';
import { profileWrapper, profileImage } from './ProfileIcon.style';
import {
profileWrapper,
profileImage,
getProfileSize,
} from './ProfileIcon.style';

export interface ProfileProps extends ComponentPropsWithRef<'button'> {
interaction: 'default' | 'custom';
imgUrl?: string;
size?: 'small' | 'large';
size?: 'large' | 'small' | 'extraSmall';
}

interface ProfilePropsWithImage extends ComponentPropsWithRef<'button'> {
interaction: 'custom';
imgUrl: string;
size?: 'small' | 'large';
size?: 'large' | 'small' | 'extraSmall';
}

const ProfileIcon = (
Expand All @@ -25,12 +29,22 @@ const ProfileIcon = (
) => {
const profileComponents = {
normal: (
<button type="button" ref={ref} css={profileWrapper(size)} {...props}>
<button
type="button"
ref={ref}
css={[profileWrapper, getProfileSize(size)]}
{...props}
>
<NormalProfile css={profileImage} />
</button>
),
settings: (
<button type="button" ref={ref} css={profileWrapper(size)} {...props}>
<button
type="button"
ref={ref}
css={[profileWrapper, getProfileSize(size)]}
{...props}
>
<img css={profileImage} src={imgUrl} alt="profile" />
</button>
),
Expand Down
30 changes: 29 additions & 1 deletion src/components/mobile/atoms/Button/Button.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ export const getVariantStyling = (
backgroundColor: color.GY[5],
color: color.GY[1],
}),
outlineHighlightR: css({
border: `2px solid ${color.PINK}`,
borderRadius: '10px',
backgroundColor: 'transparent',
color: color.RED,
}),
outlineHighlightB: css({
border: `2px solid ${color.SKYBLUE}`,
borderRadius: '10px',
backgroundColor: 'transparent',
color: color.BLUE,
}),
};

return style[variant];
Expand Down Expand Up @@ -58,6 +70,22 @@ export const getSizeByVariantStyling = (
height: '34px',
}),
},
outlineHighlightR: {
large: css({}),
medium: css(typo.Comment.SemiBold, {
width: '134px',
height: '72px',
padding: '0 21px',
}),
},
outlineHighlightB: {
large: css({}),
medium: css(typo.Comment.SemiBold, {
width: '134px',
height: '72px',
padding: '0 21px',
}),
},
};

return style[variant][size];
Expand All @@ -68,6 +96,6 @@ export const buttonStyling = css({
justifyContent: 'center',
alignItems: 'center',
border: 'none',
whiteSpace: 'nowrap',
whiteSpace: 'normal',
cursor: 'pointer',
});
7 changes: 6 additions & 1 deletion src/components/mobile/atoms/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import * as S from './Button.style';

export interface ButtonProps extends ComponentPropsWithRef<'button'> {
size?: 'large' | 'medium';
variant?: 'primary' | 'roundPrimary' | 'outlineShadow';
variant?:
| 'primary'
| 'roundPrimary'
| 'outlineShadow'
| 'outlineHighlightR'
| 'outlineHighlightB';
active?: boolean;
}

Expand Down
26 changes: 26 additions & 0 deletions src/components/mobile/atoms/SummaryItem/SummaryItem.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { css } from '@emotion/react';
import color from '@/styles/color';
import typo from '@/styles/typo';

export const summaryItemStyling = css(typo.Mobile.Text.Medium_12, {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '4px 20px 4px 5px',
gap: '13px',
backgroundColor: color.WT_VIOLET,
color: color.BK,
borderRadius: '30px',
});

export const numberItemStyling = css(typo.Mobile.Text.Medium_12, {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexShrink: 0,
width: '18px',
height: '18px',
backgroundColor: color.MAIN,
color: color.WT,
borderRadius: '50%',
});
16 changes: 16 additions & 0 deletions src/components/mobile/atoms/SummaryItem/SummaryItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React, { ReactNode } from 'react';
import { numberItemStyling, summaryItemStyling } from './SummaryItem.style';

export interface SummaryItemProps {
itemNumber?: '1' | '2' | '3';
children?: ReactNode;
}

const SummaryItem = ({ itemNumber = '1', children }: SummaryItemProps) => (
<div css={summaryItemStyling}>
<div css={numberItemStyling}>{itemNumber}</div>
{children}
</div>
);

export default SummaryItem;
76 changes: 76 additions & 0 deletions src/components/mobile/molecules/ReportModal/ReportModal.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { css } from '@emotion/react';
import color from '@/styles/color';
import typo from '@/styles/typo';

export const reportModalStyling = css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '20px',
});

export const reportTextWrapper = css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '6px',
});

export const reportTextStyling = css(typo.Main.SemiBold, {
color: color.BK,
});

export const buttonWrapperStyling = css({
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gridGap: '7px',
});

export const reportBtnWrapperStyling = css({
display: 'flex',
justifyContent: 'space-between',
width: '100%',
});

export const buttonStyling = css(typo.Mobile.Text.Medium_14, {
display: 'flex',
width: '143px',
height: '50px',
alignItems: 'center',
padding: '0 16px',
borderRadius: '8px',
backgroundColor: color.GY[5],
color: color.GY[6],
cursor: 'pointer',
});

export const selectedButtonStyling = css({
backgroundColor: color.WT_VIOLET,
color: color.MAIN,
outline: `2px solid ${color.MAIN}`,
});

export const getButtonStyling = (selected: boolean) =>
css({
width: '293px',
height: '44px',
borderRadius: '12px',
backgroundColor: selected ? color.MAIN : color.GY[2],
});
Comment on lines +53 to +59
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

선택된 버튼의 너비가 일관성이 없습니다.

getButtonStyling 함수에서 설정된 버튼 너비(293px)가 buttonStyling의 너비(143px)와 크게 다릅니다. 일관된 사용자 경험을 위해 버튼 크기를 통일하는 것이 좋습니다.

export const getButtonStyling = (selected: boolean) =>
  css({
-    width: '293px',
+    width: '120px',
    height: '44px',
    borderRadius: '12px',
    backgroundColor: selected ? color.MAIN : color.GY[2],
  });

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


export const reportInputStyling = css(typo.Mobile.Text.SemiBold_14, {
width: '100%',
padding: '6px 0',
outline: '0',
border: 'none',
borderBottom: `1px solid ${color.GY[3]}`,
color: color.BK,

':-webkit-autofill': {
boxShadow: '0 0 0px 1000px white inset',
},

'&::placeholder': {
color: color.GY[1],
},
});
75 changes: 75 additions & 0 deletions src/components/mobile/molecules/ReportModal/ReportModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { useState } from 'react';
import { MobileReport } from '@/assets';
import Modal from '@/components/mobile/atoms/Modal/Modal';
import Button from '@/components/mobile/atoms/Button/Button';
import { reportOptions } from '@/constants/reportOption';
import * as S from './ReportModal.style';

export interface ReportModalProps {
isOpen?: boolean;
onConfirm?: (reason: string) => void;
onClose?: () => void;
}
Comment on lines +8 to +12
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Props 타입에 필수값 표시가 필요합니다.

Props 타입에서 선택적 필드(?)를 사용하고 있지만, 모달의 정상적인 작동을 위해서는 이러한 props가 필수적입니다.

 export interface ReportModalProps {
-  isOpen?: boolean;
-  onConfirm?: (reason: string) => void;
-  onClose?: () => void;
+  isOpen: boolean;
+  onConfirm: (reason: string) => void;
+  onClose: () => void;
 }
📝 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 ReportModalProps {
isOpen?: boolean;
onConfirm?: (reason: string) => void;
onClose?: () => void;
}
export interface ReportModalProps {
isOpen: boolean;
onConfirm: (reason: string) => void;
onClose: () => void;
}


const ReportModal = ({ isOpen, onConfirm, onClose }: ReportModalProps) => {
const [reportReason, setReportReason] = useState<string>('');
const [otherReason, setOtherReason] = useState<string>('');
const finalReportReason: string =
reportReason === '기타' ? otherReason : reportReason;

const handleOtherReportReason = (e: React.ChangeEvent<HTMLInputElement>) => {
setOtherReason(e.target.value);
};

const handleConfirm = () => {
if (!finalReportReason.trim()) return;
onConfirm?.(finalReportReason);
};
Comment on lines +24 to +27
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

handleConfirm 함수의 오류 처리가 필요합니다.

빈 문자열 체크만 하고 있으며, 최소/최대 길이 검증이나 사용자 피드백이 없습니다.

 const handleConfirm = () => {
-  if (!finalReportReason.trim()) return;
+  if (!finalReportReason.trim()) {
+    showToastModal('신고 사유를 입력해주세요.');
+    return;
+  }
+  if (reportReason === '기타' && otherReason.length < 10) {
+    showToastModal('신고 사유는 최소 10자 이상 입력해주세요.');
+    return;
+  }
   onConfirm?.(finalReportReason);
 };
📝 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 handleConfirm = () => {
if (!finalReportReason.trim()) return;
onConfirm?.(finalReportReason);
};
const handleConfirm = () => {
if (!finalReportReason.trim()) {
showToastModal('신고 사유를 입력해주세요.');
return;
}
if (reportReason === '기타' && otherReason.length < 10) {
showToastModal('신고 사유는 최소 10자 이상 입력해주세요.');
return;
}
onConfirm?.(finalReportReason);
};


return (
<Modal action="share" isOpen={isOpen} onClose={onClose}>
<div css={S.reportModalStyling}>
<div css={S.reportTextWrapper}>
<MobileReport />
<div css={S.reportTextStyling}>신고사유 선택</div>
</div>
<div css={S.buttonWrapperStyling}>
{reportOptions.map((option) => (
<button
type="button"
value={option.value}
key={option.value}
onClick={() => {
setReportReason(option.value);
setOtherReason('');
}}
css={[
S.buttonStyling,
option.value === reportReason && S.selectedButtonStyling,
]}
>
{option.label}
</button>
))}
</div>
{reportReason === '기타' && (
<input
css={S.reportInputStyling}
placeholder="신고사유를 작성해주세요."
onChange={handleOtherReportReason}
/>
Comment on lines +56 to +60
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성을 개선하세요.

입력 필드에 적절한 레이블과 ARIA 속성이 필요합니다.

+<label htmlFor="otherReason" css={S.srOnly}>
+  기타 신고 사유
+</label>
 <input
+  id="otherReason"
   css={S.reportInputStyling}
   placeholder="신고사유를 작성해주세요."
+  aria-label="기타 신고 사유"
   onChange={handleOtherReportReason}
 />
📝 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
<input
css={S.reportInputStyling}
placeholder="신고사유를 작성해주세요."
onChange={handleOtherReportReason}
/>
<label htmlFor="otherReason" css={S.srOnly}>
기타 신고 사유
</label>
<input
id="otherReason"
css={S.reportInputStyling}
placeholder="신고사유를 작성해주세요."
aria-label="기타 신고 사유"
onChange={handleOtherReportReason}
/>

)}
Comment on lines +55 to +61
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

입력 필드에 대한 유효성 검사를 추가하세요.

기타 사유 입력 시 최소/최대 길이 제한과 같은 유효성 검사가 필요합니다.

 <input
   css={S.reportInputStyling}
   placeholder="신고사유를 작성해주세요."
+  minLength={10}
+  maxLength={200}
   onChange={handleOtherReportReason}
+  required
 />
📝 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
{reportReason === '기타' && (
<input
css={S.reportInputStyling}
placeholder="신고사유를 작성해주세요."
onChange={handleOtherReportReason}
/>
)}
{reportReason === '기타' && (
<input
css={S.reportInputStyling}
placeholder="신고사유를 작성해주세요."
minLength={10}
maxLength={200}
onChange={handleOtherReportReason}
required
/>
)}

<Button
size="large"
variant="primary"
onClick={handleConfirm}
css={S.getButtonStyling(!!finalReportReason)}
>
설정 완료
</Button>
</div>
</Modal>
);
};

export default ReportModal;
Loading
Loading