Skip to content

Commit

Permalink
✨ 피드 신고 UI (#57)
Browse files Browse the repository at this point in the history
* feat: 피드 케밥 메뉴 아이콘 분리
* feat: 케밥 메뉴 상호작용 기능 연결
* feat: 피드 케밥 메뉴 UI
* feat: useToggle 공용 훅 추가
* style: useToggle 주석 추가
* feat: useToggle 훅 적용 및 신고하기 UI
* feat: ConfirmReportModal 위치 이동
* feat: 피드 신고 양식 추가
* feat: ConfirmReportModal 스타일 적용
* feat: checkoff, checkon 아이콘 추가
* style: 피드 신고 모달창 너비값 수정
* feat: 신고 아이템 리스트 추가
* feat: 신고 아이템 체크박스 기능
* feat: 신고 리스트 스타일 적용
* feat: textarea 전역 설정 추가
* feat: 신고 사유 입력창
* feat: textarea 최대 길이 설정
* feat: checkbox 아이콘 추가 및 분리
* feat: 체크박스 숨기기 UI
* feat: isHiddenPost -> isBlind 상태명 수정
* feat: content.length 제거
* refactor: useInput 훅스 적용
* feat: useToggle 훅스 as const 적용
* feat: 신고 사유 리스트 및 훅스 네이밍 수정
* feat: 신고 카테고리 관리 훅스 타입 명시 및 유틸리티 함수 추가
* refactor: 피드 신고 양식 리팩토링
* feat: CategoryId 타입 수정
* feat: ConfirmReportModal children 타입 수정
* feat: useCallback 제거

Closes #PW-321
  • Loading branch information
BangDori authored May 14, 2024
1 parent c90845d commit 194a420
Show file tree
Hide file tree
Showing 27 changed files with 452 additions and 48 deletions.
64 changes: 64 additions & 0 deletions public/assets/sprites/common.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/app/styles/_reset.scss
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ summary,
time,
mark,
audio,
video {
video,
textarea {
margin: 0;
padding: 0;
border: 0;
Expand Down
1 change: 1 addition & 0 deletions src/features/feed-reports/consts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './reports';
18 changes: 18 additions & 0 deletions src/features/feed-reports/consts/reports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type ReportCategoryId = 1 | 2 | 3 | 4 | 5 | 6 | 7;

interface ReportCategory {
id: ReportCategoryId;
name: string;
}

export const REPORT_CATEOGRIES: ReportCategory[] = [
{ id: 1, name: '상업적/홍보성' },
{ id: 2, name: '음란/선정성' },
{ id: 3, name: '저작권 침해' },
{ id: 4, name: '개인정보 노출' },
{ id: 5, name: '욕설/인신공격' },
{ id: 6, name: '반복적인 내용' },
{ id: 7, name: '기타' },
];

export const MAX_REPORT_CONTENT_LENGTH = 100;
1 change: 1 addition & 0 deletions src/features/feed-reports/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FeedReportsForm } from './ui';
1 change: 1 addition & 0 deletions src/features/feed-reports/model/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useReportCategories';
26 changes: 26 additions & 0 deletions src/features/feed-reports/model/useReportCategories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useState } from 'react';

import { REPORT_CATEOGRIES, ReportCategoryId } from '../consts';

export const useReportCategories = () => {
const [categories, setCategories] = useState(
new Map<ReportCategoryId, boolean>(
REPORT_CATEOGRIES.map((item) => [item.id, false]),
),
);

const handleClickCategory = (id: ReportCategoryId) => {
setCategories((prev) => {
const newCheckedItem = new Map(prev);
newCheckedItem.set(id, !newCheckedItem.get(id));
return newCheckedItem;
});
};

return { categories, handleClickCategory };
};

export function getCategoryName(id: ReportCategoryId) {
const category = REPORT_CATEOGRIES.find((item) => item.id === id);
return category?.name ?? '';
}
21 changes: 21 additions & 0 deletions src/features/feed-reports/ui/ConfirmReportModal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.confirm-report-modal {
display: flex;
flex-direction: column;

border-radius: 8px;
border: none;
background-color: white;

width: 244px;
padding: 18px;

.title {
align-self: start;
}

.modal-btn-container {
display: flex;
justify-content: center;
gap: 8px;
}
}
39 changes: 39 additions & 0 deletions src/features/feed-reports/ui/ConfirmReportModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ActiveButton, BasicButton } from '@/shared/ui';
import { ModalOverlay } from '@/shared/ui/modal/ModalOverlay';

import './ConfirmReportModal.scss';

interface ConfirmReportModalProps {
onExecute: () => void;
onExecuteIsDisabled: boolean;
onClose: () => void;
children: JSX.Element[];
}

export const ConfirmReportModal: React.FC<ConfirmReportModalProps> = ({
onExecute,
onExecuteIsDisabled,
onClose,
children,
}) => {
return (
<ModalOverlay styleClass='modal' onClose={onClose}>
<form className='confirm-report-modal'>
<h3 className='title h3semi'>신고하기</h3>
{children}
<div className='modal-btn-container'>
<BasicButton onClick={onClose} styleClass='confirm-cancle h4semi'>
취소
</BasicButton>
<ActiveButton
onClick={onExecute}
isDisabled={onExecuteIsDisabled}
styleClass='confirm h4semi'
>
신고하기
</ActiveButton>
</div>
</form>
</ModalOverlay>
);
};
68 changes: 68 additions & 0 deletions src/features/feed-reports/ui/FeedReportsForm.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
.reports-list {
margin-top: 16px;

display: grid;
grid-template-columns: repeat(2, 1fr);

row-gap: 10px;

.report-item {
display: flex;
align-items: center;

gap: 4px;

.checkbox-btn {
width: 20px;
height: 20px;
}

.item-name {
color: $gray5;
}
}
}

.report-textarea-container {
margin-top: 16px;
position: relative;

width: 244px;
height: 72px;

.report-textarea {
padding: 10px;

width: calc(100% - 20px);
height: calc(100% - 20px);

background-color: $gray1;

resize: none;

&:focus {
outline: none;
}
}

.textarea-text-count {
position: absolute;

right: 10px;
bottom: 8px;

color: $gray3;
}
}

.hide-checkbox-container {
margin: 12px 0 20px;

display: flex;
align-items: center;
gap: 4px;

.hide-checkbox-text {
color: $gray5;
}
}
74 changes: 74 additions & 0 deletions src/features/feed-reports/ui/FeedReportsForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useInput, useToggle } from '@/shared/hooks';
import { Icon } from '@/shared/ui';

import { MAX_REPORT_CONTENT_LENGTH } from '../consts';
import { useReportCategories, getCategoryName } from '../model';

import { ConfirmReportModal } from './ConfirmReportModal';
import './FeedReportsForm.scss';

interface FeedReportsFormProps {
onClose: () => void;
}

export const FeedReportsForm: React.FC<FeedReportsFormProps> = ({
onClose,
}) => {
const { categories, handleClickCategory } = useReportCategories();
const [content, handleInputContent] = useInput();
const [isBlind, toggleBlind] = useToggle(false);

return (
<ConfirmReportModal
onExecute={() => {}} // API 연동 후 수정
onExecuteIsDisabled={false} // API 연동 후 수정
onClose={onClose}
>
{/* 신고 카테고리 */}
<ul className='reports-list'>
{[...categories].map(([id, checked]) => (
<li key={id} className='report-item'>
<button
className='checkbox-btn'
type='button'
onClick={() => handleClickCategory(id)}
>
<Icon
name={checked ? 'checkbox-circle_on' : 'checkbox-circle_off'}
width='20'
height='20'
/>
</button>
<p className='item-name b1md'>{getCategoryName(id)}</p>
</li>
))}
</ul>

{/* 신고 사유 */}
<div className='report-textarea-container'>
<textarea
className='report-textarea b1md'
spellCheck={false}
value={content}
onChange={handleInputContent}
maxLength={MAX_REPORT_CONTENT_LENGTH}
/>
<span className='textarea-text-count b2md'>
{content.length}/{MAX_REPORT_CONTENT_LENGTH}
</span>
</div>

{/* 숨김 처리 체크박스 */}
<div className='hide-checkbox-container'>
<button className='checkbox-btn' type='button' onClick={toggleBlind}>
<Icon
name={isBlind ? 'checkbox-square_on' : 'checkbox-square_off'}
width='20'
height='20'
/>
</button>
<p className='hide-checkbox-text b1md'>해당 게시물 숨기기</p>
</div>
</ConfirmReportModal>
);
};
1 change: 1 addition & 0 deletions src/features/feed-reports/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FeedReportsForm } from './FeedReportsForm';
2 changes: 2 additions & 0 deletions src/shared/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { useToggle } from './useToggle';
export { useInput } from './useInput';
19 changes: 19 additions & 0 deletions src/shared/hooks/useInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useCallback, useState } from 'react';

/**
* useInput 훅은 input 태그의 value와 onChange 이벤트를 처리하는 함수를 반환하는 커스텀 훅입니다.
* @param defaultValue 초기값
* @returns [상태값, 이벤트 핸들러, 상태 설정 함수]
*/
export const useInput = (defaultValue: string = '') => {
const [value, setValue] = useState(defaultValue);

const handleInputContent = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
setValue(e.target.value);
},
[],
);

return [value, handleInputContent, setValue] as const; // 자리 고정
};
14 changes: 14 additions & 0 deletions src/shared/hooks/useToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useState } from 'react';

/**
* reference: https://usehooks-ts.com/react-hook/use-toggle
* useToggle 훅은 boolean 값과 값을 토글할 수 있는 함수를 반환하는 커스텀 훅입니다.
* @param defaultValue 초기값
* @returns [상태값, 토글 함수, 상태 설정 함수]
*/
export function useToggle(defaultValue?: boolean) {
const [value, setValue] = useState(!!defaultValue);
const toggle = () => setValue((x) => !x);

return [value, toggle, setValue] as const;
}
6 changes: 5 additions & 1 deletion src/shared/ui/icon/consts/sprite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ export type IconName =
| 'chat'
| 'search'
| 'caution'
| 'no-profile';
| 'no-profile'
| 'checkbox-circle_off'
| 'checkbox-circle_on'
| 'checkbox-square_on'
| 'checkbox-square_off';
2 changes: 1 addition & 1 deletion src/shared/ui/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { ActiveButton, BasicButton } from './button';
export { PageHeader } from './header';
export { ConfirmModal, ConfirmReportModal, BottomSheetModal } from './modal';
export { ConfirmModal, BottomSheetModal } from './modal';
export { Profile, SkeletonProfile } from './profile';
export { Icon } from './icon';
export { NetworkError } from './network-error';
Expand Down
Loading

0 comments on commit 194a420

Please sign in to comment.