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

[INQUIRY] 문의하기 페이지 기능 #129

Merged
merged 33 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
adcd117
feat: 브랜치 #105 생성
junghaesung79 Dec 21, 2023
760bfd6
feat: file 업로드 버튼
junghaesung79 Dec 21, 2023
70b2ead
feat: 업로드한 이미지 렌더링
junghaesung79 Dec 21, 2023
1b1c560
feat: 제출 폼 기능 구현
junghaesung79 Dec 21, 2023
d3dac4d
feat: 문의 content 출력
junghaesung79 Dec 23, 2023
5613008
refactor: 가독성을 위한 개행 추가
junghaesung79 Dec 23, 2023
2b55338
feat: 문의 post 요청 구현
junghaesung79 Dec 23, 2023
6ae96ec
feat: 문의하기 내용을 navigate 시 문의사항에 렌더링되도록 변경
junghaesung79 Dec 23, 2023
8cec1d1
feat: 작성 중인 문의하기를 세션에 임시 저장
junghaesung79 Dec 23, 2023
0d38233
feat: 필수 항목 기입 시에만 버튼 색 highlight
junghaesung79 Dec 23, 2023
8312897
refactor: 파일 구조 수정
junghaesung79 Dec 29, 2023
0331757
refactor: 상수명 변경
junghaesung79 Dec 29, 2023
82c07eb
feat: 문의사항 보기 이미지 불러오는 로직 변경
junghaesung79 Dec 29, 2023
bbdef59
refactor: inquiry list 로직 변경
junghaesung79 Jan 3, 2024
b1061e3
refactor: 문의사항 로직 변경
junghaesung79 Jan 5, 2024
4a9de70
refactor: console.log, 주석 삭제
junghaesung79 Jan 5, 2024
5a581d3
refactor: 문의사항 로직 변경
junghaesung79 Jan 6, 2024
89336cb
refactor: lint 에러 수정
junghaesung79 Jan 6, 2024
b5919d8
refactor: 주석 삭제
junghaesung79 Jan 6, 2024
5c0b875
refactor: 웹 실행 시 오류 수정
junghaesung79 Jan 6, 2024
f8e7b79
refactor: 나의 문의사항 렌더링
junghaesung79 Jan 6, 2024
e50fa1e
refactor: 문의하기 후 리 디렉토링 시 문의사항 정상 렌더링
junghaesung79 Jan 6, 2024
3c57db6
refactor: import 컨벤션 부분 적용
junghaesung79 Jan 6, 2024
3588e28
Merge remote-tracking branch 'origin/develop' into feature/#105
junghaesung79 Jan 7, 2024
49a7719
refactor: lint 에러 수정
junghaesung79 Jan 7, 2024
8c96c7e
refactor: 객체 상태로 문의 폼 저장
junghaesung79 Jan 7, 2024
f70d3f9
refactor: 성능, 가독성 개선
junghaesung79 Jan 7, 2024
e745c13
refactor: 요청 부분 try catch 삭제
junghaesung79 Jan 8, 2024
6f0aa71
refactor: useMemo, useCallback 삭제
junghaesung79 Jan 9, 2024
0492044
refactor: 이미지 업로드 로직 변경
junghaesung79 Jan 9, 2024
9aa080c
refactor: 검색 기능 구현
junghaesung79 Jan 9, 2024
a40068e
refactor: isSubmissionReady 표현 방식 변경
junghaesung79 Jan 9, 2024
406bd2f
Merge remote-tracking branch 'origin/develop' into feature/#105
junghaesung79 Jan 9, 2024
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
9 changes: 8 additions & 1 deletion src/api/inquiry/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ export interface InquirySort {
export interface InquiryProps {
queryType: string;
dateCursor: string | null;
idCursor: number | null;
idCursor: number | 0;
size: number;
}

export interface SubmitInquiry {
title: string;
content: string;
inquiryImages?: InquiryImage[];
junghaesung79 marked this conversation as resolved.
Show resolved Hide resolved
isSecret: boolean;
}
16 changes: 0 additions & 16 deletions src/api/inquiry/index.ts

This file was deleted.

26 changes: 26 additions & 0 deletions src/api/inquiry/inquire/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SubmitInquiry } from '../entity';
import inquiryApi from '../inquiryApiClient';

const submitInquiry = async (inquiryData: SubmitInquiry) => {
const formData = new FormData();

formData.append('title', inquiryData.title);
formData.append('content', inquiryData.content);
formData.append('isSecret', String(inquiryData.isSecret));

if (inquiryData.inquiryImages) {
inquiryData.inquiryImages.forEach((imageData) => {
formData.append('inquiryImages', imageData.imageUrl);
});
}

const response = await inquiryApi.post('/inquiry', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});

return response.data;
};

export default submitInquiry;
12 changes: 12 additions & 0 deletions src/api/inquiry/inquiry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { GetInquiryResponse, InquiryProps } from '../entity';
import inquiryApi from '../inquiryApiClient';

const getInquiry = async ({
queryType, dateCursor, idCursor, size,
}: InquiryProps) => {
const queryParams = `${queryType}?dateCursor=${dateCursor}&idCursor=${idCursor}&size=${size}`;
const { data } = await inquiryApi.get<GetInquiryResponse>(`/inquiry${queryParams}`);
return data;
};

export default getInquiry;
9 changes: 9 additions & 0 deletions src/api/inquiry/inquiryApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,13 @@ const inquiryApi = axios.create({
timeout: 2000,
});

inquiryApi.interceptors.request.use(
(config) => {
const accessToken = sessionStorage.getItem('accessToken');
// eslint-disable-next-line no-param-reassign
if (config.headers && accessToken) config.headers.Authorization = `Bearer ${accessToken}`;
return config;
},
);

export default inquiryApi;
4 changes: 4 additions & 0 deletions src/assets/svg/inquiry/image-delete.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ $disabled: #c4c4c4;
border-bottom-right-radius: 3px;
position: relative;

&--attached {
height: 122px;
}

> svg {
cursor: pointer;
position: absolute;
Expand All @@ -94,12 +98,43 @@ $disabled: #c4c4c4;
}

&__upload-button {
color: transparent;
cursor: pointer;
position: absolute;
top: 16px;
left: 16px;
width: 16px;
height: 16px;
}

&__input {
display: none;
}

&__images {
height: 90px;
position: absolute;
left: 48px;
display: flex;
flex-direction: row;
}

&__image-box {
margin-right: 32px;
position: relative;
}

&__image {
width: 90px;
height: 90px;
border-radius: 10px;
}

&__delete-button {
position: absolute;
top: -7px;
right: -7px;
cursor: pointer;
}

&__description {
Expand Down Expand Up @@ -139,18 +174,23 @@ $disabled: #c4c4c4;
justify-content: center;
width: 176px;
height: 44px;
background: $highlight;
background: $disabled;
border: none;
appearance: none;
border-radius: 22px;
font-weight: 700;
font-size: 16px;
text-decoration: none;
color: white;
cursor: pointer;
cursor: default;
box-shadow: 2px 3px 12px 1px rgba(0 0 0 / 10%);
margin-top: 48px;
position: absolute;
right: 0;

&--active {
background: $highlight;
cursor: pointer;
}
}
}
159 changes: 138 additions & 21 deletions src/pages/Inquiry/Inquire/components/InquireForm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,100 @@
import { useState } from 'react';
import {
useCallback, useEffect, useMemo, useState,
} from 'react';
import { useLocation } from 'react-router-dom';

import { InquiryImage, SubmitInquiry } from 'api/inquiry/entity';
import { ReactComponent as DeleteIcon } from 'assets/svg/inquiry/image-delete.svg';
import { ReactComponent as UploadIcon } from 'assets/svg/inquiry/image-upload.svg';
import ToggleButton from 'components/common/ToggleButton';
import RequiredLabel from 'pages/Inquiry/Inquire/components/InquireForm/components/RequiredLabel/index';
import useBooleanState from 'utils/hooks/useBooleanState';
import useSubmitInquiry from 'pages/Inquiry/hooks/useSubmitInquiry';
import RequiredLabel from 'pages/Inquiry/Inquire/components/InquireForm/RequiredLabel';
import cn from 'utils/ts/classNames';
import makeToast from 'utils/ts/makeToast';

import styles from './InquireForm.module.scss';

const MAX_LENGTH = 500;

export default function InquireForm(): JSX.Element {
const [content, setContent] = useState('');
const maxLength = 500;
const [isSecret, , , toggle] = useBooleanState(false);
const submit = useSubmitInquiry();
const location = useLocation();

const [inquiry, setInquiry] = useState<SubmitInquiry>({
title: '',
content: '',
inquiryImages: [],
isSecret: false,
junghaesung79 marked this conversation as resolved.
Show resolved Hide resolved
});

const isAttached = useMemo(
() => !!inquiry.inquiryImages && inquiry.inquiryImages.length > 0,
[inquiry.inquiryImages],
);

junghaesung79 marked this conversation as resolved.
Show resolved Hide resolved
const isSubmissionReady = useMemo(
() => !!(inquiry.title.trim() && inquiry.content.trim()),
[inquiry.title, inquiry.content],
junghaesung79 marked this conversation as resolved.
Show resolved Hide resolved
);

const handleUpload = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
const url = URL.createObjectURL(event.target.files[0]);
const data: InquiryImage = {
imageUrl: url,
originalName: url, // 적당한 값
path: url, // 적당한 값
};

const handleContentChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setContent(event.target.value);
};
if (inquiry.inquiryImages && inquiry.inquiryImages.length < 3) {
setInquiry((prev) => (
prev.inquiryImages
? { ...prev, inquiryImages: [...prev.inquiryImages, data] }
: { ...prev, inquiryImages: [data] }
));
}
}
}, [inquiry.inquiryImages, setInquiry]);

const handleDeleteImage = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
const index = parseInt(event.currentTarget.name, 10);
setInquiry((prev) => ({
...prev,
inquiryImages: prev.inquiryImages ? prev.inquiryImages.filter((_, i) => i !== index) : [],
}));
}, [setInquiry]);

const handleSubmit = useCallback((event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (isSubmissionReady) {
sessionStorage.removeItem('inquiryForm');
submit(inquiry);
} else {
makeToast('error', '필수 항목을 기입해주세요.');
}
}, [isSubmissionReady, inquiry, submit]);

useEffect(() => {
const savedData = sessionStorage.getItem('inquiryForm');
if (savedData) {
setInquiry(JSON.parse(savedData));
}
}, [location]);

useEffect(() => {
const debouncedSave = setTimeout(() => {
sessionStorage.setItem('inquiryForm', JSON.stringify(inquiry));
}, 1000);
junghaesung79 marked this conversation as resolved.
Show resolved Hide resolved

return () => clearTimeout(debouncedSave);
}, [inquiry]);

return (
<div className={styles.container}>
<form className={styles.form}>
<form
className={styles.form}
onSubmit={handleSubmit}
>
<div className={styles.title}>
<RequiredLabel text="제목" />
<input
Expand All @@ -29,13 +105,15 @@ export default function InquireForm(): JSX.Element {
id="inquiryTitle"
maxLength={50}
placeholder="제목을 작성해주세요."
value={inquiry.title}
onChange={(e) => setInquiry((prev) => ({ ...prev, title: e.target.value }))}
/>
</div>

<div className={styles.contents}>
<RequiredLabel text="문의 내용" />
<div className={styles.contents__length}>
{`${content.length}/${maxLength}`}
{`${inquiry.content.length}/${MAX_LENGTH}`}
</div>
</div>
<div className={styles.contents__inputs}>
Expand All @@ -47,19 +125,52 @@ export default function InquireForm(): JSX.Element {
rows={10}
maxLength={500}
placeholder="문의 내용을 작성해주세요."
value={content}
onChange={handleContentChange}
value={inquiry.content}
onChange={(e) => setInquiry((prev) => ({ ...prev, content: e.target.value }))}
/>
<div className={styles.contents__attach}>
<UploadIcon />
<input
<div className={cn({
[styles.contents__attach]: true,
[styles['contents__attach--attached']]: isAttached,
})}
>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label
className={styles['contents__upload-button']}
htmlFor="upload"
>
<UploadIcon />
</label>
<input
className={styles.contents__input}
id="upload"
type="file"
accept="image/*"
aria-label="이미지 업로드"
onChange={handleUpload}
/>
<div className={styles.contents__images}>a</div>
<span className={styles.contents__description}>최대 3장 첨부 가능</span>
<div className={styles.contents__images}>
{inquiry.inquiryImages?.map((imageData, index) => (
<div className={styles['contents__image-box']} key={imageData.imageUrl}>
<img
className={styles.contents__image}
src={imageData.imageUrl}
alt={`문의 이미지${index}`}
/>
<button
className={styles['contents__delete-button']}
type="button"
aria-label="delete"
name={`${index}`}
onClick={handleDeleteImage}
>
junghaesung79 marked this conversation as resolved.
Show resolved Hide resolved
<DeleteIcon />
</button>
</div>
))}
</div>
{isAttached ? null : (
<span className={styles.contents__description}>최대 3장 첨부 가능</span>
)}
</div>
</div>

Expand All @@ -69,16 +180,22 @@ export default function InquireForm(): JSX.Element {
</span>
<ToggleButton
className={styles['secret__toggle-button']}
active={isSecret}
toggle={toggle}
active={inquiry.isSecret}
toggle={() => setInquiry((prev) => ({ ...prev, isSecret: !prev.isSecret }))}
/>
</div>
<div className={styles.secret__description}>
비밀글을 나의 문의 내역에서만 볼 수 있어요.
</div>

<div className={styles.submit}>
<button type="submit" className={styles.submit__button}>
<button
className={cn({
[styles.submit__button]: true,
[styles['submit__button--active']]: isSubmissionReady,
})}
type="submit"
>
등록하기
</button>
</div>
Expand Down
Loading
Loading