Skip to content

Commit 2e9601e

Browse files
authored
Merge pull request #196 from DevKor-github/feat/#154/report
feat: 신고하기
2 parents cc4db70 + 1699f7d commit 2e9601e

9 files changed

Lines changed: 299 additions & 29 deletions

File tree

src/features/chatList/components/ChatList/index.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as s from './style.css';
2-
import { Link } from 'react-router';
2+
import { Link, useNavigate } from 'react-router';
33
import SchoolVerifiedTag from '@/common/components/SchoolVerifiedTag';
44
import { UserProfileImage } from '@/common/components/UserProfileImage';
55
import { getKoreanRelativeTime } from '@/common/utils/getKoreanRelativeTime';
@@ -19,6 +19,7 @@ export interface Props {
1919
}
2020

2121
const ChatList = ({ data }: Props) => {
22+
const navigate = useNavigate();
2223
const { open, drawerState, close } = useDrawer();
2324
const { mutate: exitChatRoom } = usePatchExit();
2425

@@ -45,7 +46,13 @@ const ChatList = ({ data }: Props) => {
4546
};
4647

4748
const onReport = () => {
48-
console.log('신고하기');
49+
navigate(`/report/${data.opponentUserId}`, {
50+
state: {
51+
reportedUserId: data.opponentUserId,
52+
itemId: data.itemId,
53+
location: 'CHATROOM',
54+
},
55+
});
4956
};
5057

5158
const exitChat = async () => {

src/features/chatList/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface ChatListInterface {
22
chatRoomId: number;
3+
itemId: number;
34
myUserId: number;
45
opponentUserId: number;
56
opponentNickname: string;

src/features/detail/components/DetailHeader/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ const DetailHeader = ({ state }: Props) => {
2222
<button className={`mgc_left_line ${s.BackButton}`} onClick={goBack} aria-label="Go back" />
2323
<div className={s.RightSide}>
2424
<button className={`${s.RightButton} mgc_share_3_fill`} aria-label="Share item" />
25-
{state.itemInfo.mine && <button className={`${s.RightButton} mgc_more_2_fill`} onClick={open} />}
25+
<button className={`${s.RightButton} mgc_more_2_fill`} onClick={open} />
2626
</div>
2727
</header>
28-
<OptionDrawer drawerState={drawerState} state={state} />
28+
<OptionDrawer drawerState={drawerState} state={state} isMine={state.itemInfo.mine} />
2929
</>
3030
);
3131
};

src/features/detail/components/OptionDrawer/index.tsx

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import type { ItemDetailInterface } from '../../types';
1212
interface Props {
1313
drawerState: DrawerState;
1414
state: ItemDetailInterface;
15+
isMine: boolean;
1516
}
16-
const OptionDrawer = ({ drawerState, state }: Props) => {
17+
const OptionDrawer = ({ drawerState, state, isMine }: Props) => {
1718
const navigate = useNavigate();
1819
const { mutate } = useDeleteItem();
1920
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
@@ -27,30 +28,62 @@ const OptionDrawer = ({ drawerState, state }: Props) => {
2728
});
2829
};
2930

31+
const MyDrawer = () => {
32+
return (
33+
<>
34+
<Drawer drawerState={drawerState}>
35+
<div className={s.Container}>
36+
<Link className={s.Button} to={'/post-edit'} state={state}>
37+
<span className="mgc_pencil_fill" />
38+
수정하기
39+
</Link>
40+
<div className={s.Divider} />
41+
<button className={s.Button} onClick={() => setShowDeleteAlert(true)}>
42+
<span className="mgc_delete_2_fill" />
43+
삭제하기
44+
</button>
45+
</div>
46+
</Drawer>
47+
{showDeleteAlert && (
48+
<CustomAlert
49+
title="삭제한 글은 다시 볼 수 없어요!"
50+
subTitle="정말 글을 삭제하실 건가요?"
51+
yesBtn="네, 삭제할게요"
52+
onNo={() => setShowDeleteAlert(false)}
53+
onYes={deleteItem}
54+
/>
55+
)}
56+
</>
57+
);
58+
};
59+
60+
const OtherDrawer = () => {
61+
return (
62+
<>
63+
<Drawer drawerState={drawerState}>
64+
<div className={s.Container}>
65+
<Link
66+
className={s.Button}
67+
to={`/report/${state.owner.id}`}
68+
state={{
69+
reportedUserId: state.owner.id,
70+
itemId: state.itemId,
71+
location: 'POST',
72+
}}
73+
>
74+
<span className="mgc_warning_fill" />
75+
신고하기
76+
</Link>
77+
</div>
78+
</Drawer>
79+
</>
80+
);
81+
};
82+
3083
return (
3184
<>
32-
<Drawer drawerState={drawerState}>
33-
<div className={s.Container}>
34-
<Link className={s.Button} to={'/post-edit'} state={state}>
35-
<span className="mgc_pencil_fill" />
36-
수정하기
37-
</Link>
38-
<div className={s.Divider} />
39-
<button className={s.Button} onClick={() => setShowDeleteAlert(true)}>
40-
<span className="mgc_delete_2_fill" />
41-
삭제하기
42-
</button>
43-
</div>
44-
</Drawer>
45-
{showDeleteAlert && (
46-
<CustomAlert
47-
title="삭제한 글은 다시 볼 수 없어요!"
48-
subTitle="정말 글을 삭제하실 건가요?"
49-
yesBtn="네, 삭제할게요"
50-
onNo={() => setShowDeleteAlert(false)}
51-
onYes={deleteItem}
52-
/>
53-
)}
85+
{isMine && <MyDrawer />}
86+
{!isMine && <OtherDrawer />}
5487
</>
5588
);
5689
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import client from '@/common/utils/client';
2+
import { useMutation } from '@tanstack/react-query';
3+
4+
export const Category_ARRAY = ['OFFENSIVE', 'SEXUAL_CONTENT', 'FAKE_ITEM', 'PRIVACY', 'SPAM', 'OTHER'] as const;
5+
export type CategoryType = (typeof Category_ARRAY)[number];
6+
7+
interface PostInterface {
8+
reportedUserId: number;
9+
itemId: number;
10+
location: 'POST' | 'CHATROOM';
11+
categories: CategoryType[];
12+
description: string | null;
13+
}
14+
15+
const postReport = async ({ body }: { body: PostInterface }) => {
16+
console.log(body);
17+
18+
const res = await client.post('api/v1/user/report', body);
19+
20+
return res.data;
21+
};
22+
23+
export const usePostReport = () => {
24+
return useMutation({
25+
mutationFn: postReport,
26+
});
27+
};

src/pages/ChatPage/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import SafeArea from '@/common/components/SafeArea';
22
import * as s from './style.css';
33
import ChatList from '@/features/chatList/components/ChatList';
44
import useGetChatList from '@/features/chatList/api/useGetChatList';
5-
import ChatTopBar from '@/features/chatList/components/ChatTopBar';
65
import Pagination from '@/common/components/Pagination';
76
import { CHAT_PAGING_SIZE } from '@/libs/constants';
87
import { useEffect } from 'react';
98
import { connectSocket, subChatListSocket } from '@/common/utils/wsClient';
109
import { useQueryClient } from '@tanstack/react-query';
1110
import { QUERY_KEYS } from '@/libs/queryKeys';
1211
import NoResult from '@/common/components/NoResult';
12+
import MainTopBar from '@/common/components/MainTopBar';
1313

1414
const ChatPage = () => {
1515
const {
@@ -41,7 +41,7 @@ const ChatPage = () => {
4141

4242
return (
4343
<SafeArea>
44-
<ChatTopBar />
44+
<MainTopBar />
4545
<div className={s.Wrapper({ isEmpty })}>
4646
{isEmpty ? (
4747
<NoResult type="chat-list" />

src/pages/ReportPage/index.tsx

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import CustomHeader from '@/common/components/CustomHeader';
2+
import SafeArea from '@/common/components/SafeArea';
3+
import { useLocation, useNavigate } from 'react-router';
4+
import * as s from './style.css';
5+
import Btn from '@/common/components/Button';
6+
import { cx } from '@styled-system/css';
7+
import { useState } from 'react';
8+
import MultilineInputfield from '@/features/post/components/MultilineInputField';
9+
import { usePostReport, type CategoryType } from '@/features/report/apis/usePostReport';
10+
11+
interface Category {
12+
key: CategoryType;
13+
label: string;
14+
checked: boolean;
15+
}
16+
17+
interface ReportPageState {
18+
reportedUserId: number;
19+
itemId: number;
20+
location: 'CHATROOM' | 'POST';
21+
}
22+
23+
const ReportPage = () => {
24+
const navigate = useNavigate();
25+
const location = useLocation();
26+
const state = location.state as ReportPageState;
27+
28+
const ownerId = state.reportedUserId;
29+
const itemId = state.itemId;
30+
const { mutate: postReport } = usePostReport();
31+
32+
const initialCategories: Category[] = [
33+
{ key: 'OFFENSIVE', label: '부적절한 언어 및 비속어를 사용했어요.', checked: false },
34+
{ key: 'SEXUAL_CONTENT', label: '음란물 또는 성적인 콘텐츠를 포함하고 있어요.', checked: false },
35+
{ key: 'FAKE_ITEM', label: '허위 매물 및 정보를 포함하고 있어요.', checked: false },
36+
{ key: 'PRIVACY', label: '본인 또는 타인의 개인정보를 유출했어요.', checked: false },
37+
{ key: 'SPAM', label: '스팸 및 광고성 게시물이에요.', checked: false },
38+
{ key: 'OTHER', label: '기타(직접 입력)', checked: false },
39+
];
40+
41+
const [categories, setCategories] = useState<Category[]>(initialCategories);
42+
const [desc, setDesc] = useState<string>('');
43+
const isOtherChecked = categories.find(c => c.key === 'OTHER')?.checked ?? false;
44+
const isOtherValid = isOtherChecked && desc.trim() !== '';
45+
46+
const isAnyChecked = (!isOtherChecked && categories.some(c => c.checked && c.key !== 'OTHER')) || isOtherValid;
47+
const btnMode: 'main' | 'default' | 'back' | 'disabled' = isAnyChecked ? 'main' : 'disabled';
48+
49+
const toggleCategory = (key: string) => {
50+
setCategories(prev => prev.map(cat => (cat.key === key ? { ...cat, checked: !cat.checked } : cat)));
51+
};
52+
53+
const report = () => {
54+
const selected = categories.filter(c => c.checked).map(c => c.key);
55+
56+
postReport(
57+
{
58+
body: {
59+
categories: selected,
60+
description: desc,
61+
itemId: itemId,
62+
location: state.location,
63+
reportedUserId: ownerId,
64+
},
65+
},
66+
{
67+
onSuccess: () => {
68+
navigate(-1);
69+
// 토스트 띄워주기
70+
},
71+
},
72+
);
73+
};
74+
75+
const handleDesc = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
76+
setDesc(e.target.value);
77+
};
78+
79+
const Content = ({ category }: { category: Category }) => {
80+
const { key, label, checked } = category;
81+
82+
return (
83+
<button className={s.SubTitle} onClick={() => toggleCategory(key)}>
84+
{label}
85+
{checked ? (
86+
<div className={cx('mgc_checkbox_fill', s.Icon)} />
87+
) : (
88+
<div className={s.CheckBoxContainer}>
89+
<div className={s.CheckBox} />
90+
</div>
91+
)}
92+
</button>
93+
);
94+
};
95+
96+
return (
97+
<SafeArea>
98+
<CustomHeader onClick={() => navigate(-1)} title="신고하기" />
99+
<div className={s.Wrapper}>
100+
<div className={s.Title}>사용자 신고 사유를 선택해 주세요.</div>
101+
<div className={s.Content}>
102+
{categories.map(cat => (
103+
<Content key={cat.key} category={cat} />
104+
))}
105+
{isOtherChecked && (
106+
<MultilineInputfield
107+
placeholder="신고 내용을 작성해 주세요."
108+
maxLength={500}
109+
value={desc}
110+
onChange={handleDesc}
111+
/>
112+
)}
113+
</div>
114+
</div>
115+
<div className={s.Btn}>
116+
<Btn mode={btnMode} onClick={report}>
117+
제출하기
118+
</Btn>
119+
</div>
120+
</SafeArea>
121+
);
122+
};
123+
124+
export default ReportPage;

src/pages/ReportPage/style.css.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { css } from '@styled-system/css';
2+
3+
export const Wrapper = css({
4+
p: '2.8rem 1rem 3.75rem 1rem',
5+
display: 'flex',
6+
flex: 1,
7+
flexDir: 'column',
8+
gap: '2.5rem',
9+
height: 'calc(100% - {sizes.NAVIGATOR_HEIGHT} - {sizes.HEADER_HEIGHT})',
10+
});
11+
12+
export const Title = css({
13+
color: '100',
14+
fontFamily: ' Pretendard',
15+
fontSize: '1.25rem',
16+
fontStyle: 'normal',
17+
fontWeight: 600,
18+
lineHeight: 'normal',
19+
letterSpacing: '-0.05rem',
20+
});
21+
22+
export const Content = css({
23+
display: 'flex',
24+
flex: 1,
25+
flexDir: 'column',
26+
gap: '1rem',
27+
});
28+
29+
export const SubTitle = css({
30+
display: 'flex',
31+
justifyContent: 'space-between',
32+
33+
color: '100',
34+
fontFamily: ' Pretendard',
35+
fontSize: '1rem',
36+
fontStyle: 'normal',
37+
fontWeight: 500,
38+
lineHeight: 'normal',
39+
letterSpacing: '-0.04rem',
40+
});
41+
42+
export const CheckBoxContainer = css({
43+
w: '1.5rem',
44+
h: '1.5rem',
45+
p: '0.2rem 0.19rem 0.17rem 0.19rem',
46+
display: 'flex',
47+
justifyContent: 'center',
48+
alignItems: 'center',
49+
});
50+
51+
export const CheckBox = css({
52+
w: '1.125rem',
53+
h: '1.125rem',
54+
flexShrink: 0,
55+
aspectRatio: '1/1',
56+
borderRadius: '0.125rem',
57+
border: '2.5px solid var(--20, rgba(255, 255, 255, 0.20))',
58+
});
59+
60+
export const Icon = css({
61+
w: '1.5rem',
62+
h: '1.5rem',
63+
display: 'flex',
64+
justifyContent: 'center',
65+
alignItems: 'center',
66+
fontSize: '1.5rem',
67+
color: '80',
68+
flexShrink: 0,
69+
});
70+
71+
export const Btn = css({
72+
p: '0 1rem',
73+
});

0 commit comments

Comments
 (0)