diff --git a/src/api/inquiry.api.ts b/src/api/inquiry.api.ts
new file mode 100644
index 00000000..0c96ee99
--- /dev/null
+++ b/src/api/inquiry.api.ts
@@ -0,0 +1,14 @@
+import { httpClient } from './http.api';
+
+export const postInquiry = async (formData: FormData) => {
+ try {
+ const response = await httpClient.post('/inquiry', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+ console.log(response);
+ } catch (e) {
+ console.log('문의하기 에러', e);
+ }
+};
diff --git a/src/components/common/header/Header.tsx b/src/components/common/header/Header.tsx
index b8523038..5a63f191 100644
--- a/src/components/common/header/Header.tsx
+++ b/src/components/common/header/Header.tsx
@@ -74,7 +74,7 @@ function Header() {
공고관리
-
+
문의하기
e.preventDefault()}>
diff --git a/src/components/home/searchFiltering/filteringContents/FilteringContents.styled.ts b/src/components/home/searchFiltering/filteringContents/FilteringContents.styled.ts
index f20ae36d..2e02dcc9 100644
--- a/src/components/home/searchFiltering/filteringContents/FilteringContents.styled.ts
+++ b/src/components/home/searchFiltering/filteringContents/FilteringContents.styled.ts
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
export const Container = styled.div`
width: 100%;
@@ -35,7 +35,7 @@ export const SkillTagButtonWrapper = styled.div`
z-index: 1000;
`;
-export const SkillTagButton = styled.button`
+export const SkillTagButton = styled.button<{ $isOpen: boolean }>`
border-radius: 1.5rem;
width: 100%;
height: 100%;
@@ -43,6 +43,16 @@ export const SkillTagButton = styled.button`
display: flex;
justify-content: space-between;
align-items: center;
+
+ svg {
+ transition: transform 300ms ease-in-out;
+ transform: rotate(0deg);
+ ${({ $isOpen }) =>
+ $isOpen &&
+ css`
+ transform: rotate(180deg);
+ `}
+ }
`;
export const SkillTagBoxWrapper = styled.div`
diff --git a/src/components/home/searchFiltering/filteringContents/FilteringContents.tsx b/src/components/home/searchFiltering/filteringContents/FilteringContents.tsx
index 32dad24f..827d19ae 100644
--- a/src/components/home/searchFiltering/filteringContents/FilteringContents.tsx
+++ b/src/components/home/searchFiltering/filteringContents/FilteringContents.tsx
@@ -40,7 +40,10 @@ export default function FilteringContents() {
return (
-
+
언어선택
diff --git a/src/components/home/searchFiltering/filteringContents/filtering/Filtering.styled.ts b/src/components/home/searchFiltering/filteringContents/filtering/Filtering.styled.ts
index 47edbe94..07b813b3 100644
--- a/src/components/home/searchFiltering/filteringContents/filtering/Filtering.styled.ts
+++ b/src/components/home/searchFiltering/filteringContents/filtering/Filtering.styled.ts
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
export const Container = styled.div``;
@@ -16,7 +16,7 @@ export const RefWrapper = styled.div`
border-radius: 1.5rem;
`;
-export const DefaultValueButton = styled.button`
+export const DefaultValueButton = styled.button<{ $isOpen: boolean }>`
width: 100%;
height: 100%;
display: flex;
@@ -24,6 +24,16 @@ export const DefaultValueButton = styled.button`
align-items: center;
padding: 0 1rem;
border-radius: 1.5rem;
+
+ svg {
+ transition: transform 300ms ease-in-out;
+ transform: rotate(0deg);
+ ${({ $isOpen }) =>
+ $isOpen &&
+ css`
+ transform: rotate(180deg);
+ `}
+ }
`;
export const SelectWrapper = styled.div`
diff --git a/src/components/home/searchFiltering/filteringContents/filtering/Filtering.tsx b/src/components/home/searchFiltering/filteringContents/filtering/Filtering.tsx
index 28edb79a..585c7f94 100644
--- a/src/components/home/searchFiltering/filteringContents/filtering/Filtering.tsx
+++ b/src/components/home/searchFiltering/filteringContents/filtering/Filtering.tsx
@@ -73,7 +73,10 @@ export default function Filtering({ selects, defaultValue }: FilteringProps) {
-
+
{changeValue}
diff --git a/src/components/mypage/ContentTab.tsx b/src/components/mypage/ContentTab.tsx
index e99abce3..9a2ab23e 100644
--- a/src/components/mypage/ContentTab.tsx
+++ b/src/components/mypage/ContentTab.tsx
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import * as S from './ContentTab.styled';
import { Link, Outlet, useLocation } from 'react-router-dom';
import { ROUTES } from '../../constants/routes';
-import MovedInquiredLink from '../common/customerService/MoveInquiredLink';
+import MovedInquiredLink from '../../pages/customerService/MoveInquiredLink';
interface Filter {
title: string;
diff --git a/src/components/mypage/activityLog/ActivityLog.tsx b/src/components/mypage/activityLog/ActivityLog.tsx
index 50fab35f..3bf257a7 100644
--- a/src/components/mypage/activityLog/ActivityLog.tsx
+++ b/src/components/mypage/activityLog/ActivityLog.tsx
@@ -1,10 +1,10 @@
-import { activityFilter } from '../../../constants/myPageFilter';
+import { ACTIVITY_FILTER } from '../../../constants/myPageFilter';
import ContentTab from '../ContentTab';
export default function ActivityLog() {
return (
<>
-
+
>
);
}
diff --git a/src/components/mypage/notifications/Notifications.tsx b/src/components/mypage/notifications/Notifications.tsx
index 32fe1b85..dd0ed37d 100644
--- a/src/components/mypage/notifications/Notifications.tsx
+++ b/src/components/mypage/notifications/Notifications.tsx
@@ -1,8 +1,8 @@
-import { notificationFilter } from '../../../constants/myPageFilter';
+import { NOTIFICATION_FILTER } from '../../../constants/myPageFilter';
import ContentTab from '../ContentTab';
export default function Notifications() {
return (
-
+
);
}
diff --git a/src/components/userPage/joinedProject/UserJoinProject.tsx b/src/components/userPage/joinedProject/UserJoinProject.tsx
index ba46642c..05acafbd 100644
--- a/src/components/userPage/joinedProject/UserJoinProject.tsx
+++ b/src/components/userPage/joinedProject/UserJoinProject.tsx
@@ -22,9 +22,9 @@ const UserJoinProject = () => {
return (
-
- 참여한 프로젝트 리스트
-
+
+ 참여한 프로젝트 리스트
+
{userJoinedProjectListData?.acceptedProjects &&
userJoinedProjectListData?.acceptedProjects?.length > 0 ? (
@@ -44,9 +44,9 @@ const UserJoinProject = () => {
)}
-
- 기획한 프로젝트 리스트
-
+
+ 기획한 프로젝트 리스트
+
{userJoinedProjectListData?.ownProjects &&
userJoinedProjectListData?.ownProjects?.length > 0 ? (
diff --git a/src/constants/customerService.ts b/src/constants/customerService.ts
new file mode 100644
index 00000000..295003d1
--- /dev/null
+++ b/src/constants/customerService.ts
@@ -0,0 +1,14 @@
+export const INQUIRY_CATEGORY = [
+ { title: '사이트 오류' },
+ { title: '공고모집' },
+ { title: '제안하기' },
+ { title: '기타' },
+] as const;
+
+export const EMPTY_IMAGE =
+ 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' as const;
+
+export const INQUIRY_MESSAGE = {
+ categoryDefault: '카테고리',
+ fileDefault: '선택된 파일이 없습니다.',
+};
diff --git a/src/constants/myPageFilter.ts b/src/constants/myPageFilter.ts
index a31a4dcb..67c6edbe 100644
--- a/src/constants/myPageFilter.ts
+++ b/src/constants/myPageFilter.ts
@@ -1,6 +1,6 @@
import { ROUTES } from './routes';
-export const notificationFilter = [
+export const NOTIFICATION_FILTER = [
{ title: '전체', url: ``, id: 0 },
{
title: '지원한 프로젝트',
@@ -22,7 +22,7 @@ export const notificationFilter = [
},
] as const;
-export const activityFilter = [
+export const ACTIVITY_FILTER = [
{ title: '내 댓글', url: ROUTES.comments, id: 0 },
{ title: '내 문의글', url: ROUTES.activityInquiries, id: 1 },
] as const;
diff --git a/src/hooks/usePostInquiry.ts b/src/hooks/usePostInquiry.ts
new file mode 100644
index 00000000..23520ae9
--- /dev/null
+++ b/src/hooks/usePostInquiry.ts
@@ -0,0 +1,16 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { postInquiry } from '../api/inquiry.api';
+import { AxiosError } from 'axios';
+
+export const usePostInquiry = () => {
+ const queryClient = useQueryClient();
+
+ const mutate = useMutation({
+ mutationFn: (formData) => postInquiry(formData),
+ onSuccess: () => {
+ queryClient.invalidateQueries();
+ },
+ });
+
+ return mutate;
+};
diff --git a/src/models/inquiry.ts b/src/models/inquiry.ts
new file mode 100644
index 00000000..6899e943
--- /dev/null
+++ b/src/models/inquiry.ts
@@ -0,0 +1,5 @@
+export interface InquiryFormData {
+ category: string;
+ title: string;
+ content: string;
+}
diff --git a/src/models/userProject.ts b/src/models/userProject.ts
index f843a084..e1aa1ab0 100644
--- a/src/models/userProject.ts
+++ b/src/models/userProject.ts
@@ -27,7 +27,7 @@ export interface ApiAppliedProject extends ApiCommonType {
export interface SelectUserProject {
acceptedProjects: JoinedProject[];
- ownProjects: AppliedProject[];
+ ownProjects: JoinedProject[];
}
export interface ApiSelectUserProject extends ApiCommonType {
diff --git a/src/components/common/customerService/CustomerServiceHeader.styled.ts b/src/pages/customerService/CustomerServiceHeader.styled.ts
similarity index 100%
rename from src/components/common/customerService/CustomerServiceHeader.styled.ts
rename to src/pages/customerService/CustomerServiceHeader.styled.ts
diff --git a/src/components/common/customerService/CustomerServiceHeader.tsx b/src/pages/customerService/CustomerServiceHeader.tsx
similarity index 100%
rename from src/components/common/customerService/CustomerServiceHeader.tsx
rename to src/pages/customerService/CustomerServiceHeader.tsx
diff --git a/src/components/common/customerService/MoveInquiredLink.styled.ts b/src/pages/customerService/MoveInquiredLink.styled.ts
similarity index 100%
rename from src/components/common/customerService/MoveInquiredLink.styled.ts
rename to src/pages/customerService/MoveInquiredLink.styled.ts
diff --git a/src/components/common/customerService/MoveInquiredLink.tsx b/src/pages/customerService/MoveInquiredLink.tsx
similarity index 77%
rename from src/components/common/customerService/MoveInquiredLink.tsx
rename to src/pages/customerService/MoveInquiredLink.tsx
index 7361b8aa..331ea4ee 100644
--- a/src/components/common/customerService/MoveInquiredLink.tsx
+++ b/src/pages/customerService/MoveInquiredLink.tsx
@@ -1,4 +1,4 @@
-import { ROUTES } from '../../../constants/routes';
+import { ROUTES } from '../../constants/routes';
import * as S from './MoveInquiredLink.styled';
export default function MovedInquiredLink() {
diff --git a/src/components/common/customerService/faq/FAQ.styled.ts b/src/pages/customerService/faq/FAQ.styled.ts
similarity index 100%
rename from src/components/common/customerService/faq/FAQ.styled.ts
rename to src/pages/customerService/faq/FAQ.styled.ts
diff --git a/src/components/common/customerService/faq/FAQ.tsx b/src/pages/customerService/faq/FAQ.tsx
similarity index 100%
rename from src/components/common/customerService/faq/FAQ.tsx
rename to src/pages/customerService/faq/FAQ.tsx
diff --git a/src/pages/customerService/inquiry/Inquiry.styled.ts b/src/pages/customerService/inquiry/Inquiry.styled.ts
new file mode 100644
index 00000000..ba8f1e47
--- /dev/null
+++ b/src/pages/customerService/inquiry/Inquiry.styled.ts
@@ -0,0 +1,190 @@
+import styled, { css } from 'styled-components';
+
+export const Container = styled.main`
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+`;
+
+export const Header = styled.header`
+ margin: 1rem 0;
+ display: flex;
+ justify-content: center;
+`;
+
+export const HeaderTitle = styled.h1``;
+
+export const InquiryForm = styled.form`
+ width: 100%;
+ display: flex;
+ justify-content: center;
+`;
+
+export const InquiryWrapper = styled.div`
+ width: 49rem;
+`;
+
+export const Nav = styled.nav`
+ width: 100%;
+ display: flex;
+ gap: 0.5rem;
+`;
+
+export const CategoryWrapper = styled.div`
+ position: relative;
+`;
+
+export const CategorySelect = styled.button<{ $isOpen: boolean }>`
+ padding: 0.3rem 0.5rem;
+ width: 9rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 1.3rem;
+ border: 1px solid ${({ theme }) => theme.color.border};
+ border-radius: ${({ theme }) => theme.borderRadius.primary};
+
+ svg {
+ width: 1.3rem;
+ height: 1.3rem;
+ transition: transform 300ms ease-in-out;
+ transform: rotate(0deg);
+ ${({ $isOpen }) =>
+ $isOpen &&
+ css`
+ transform: rotate(180deg);
+ `}
+ }
+`;
+
+export const CategoryValueInput = styled.input`
+ position: absolute;
+ width: 0;
+ height: 0;
+ overflow: hidden;
+`;
+
+export const CategoryButtonWrapper = styled.div`
+ width: 9rem;
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ border: 1px solid ${({ theme }) => theme.color.border};
+ border-radius: ${({ theme }) => theme.borderRadius.primary};
+ background: ${({ theme }) => theme.color.white};
+`;
+
+export const CategoryButton = styled.button`
+ font-size: 1.3rem;
+ padding: 0.5rem;
+ display: flex;
+ justify-content: start;
+ align-items: center;
+
+ &:hover {
+ background: ${({ theme }) => theme.color.navy};
+ color: ${({ theme }) => theme.color.white};
+ }
+`;
+
+export const InputInquiryTitle = styled.input`
+ padding: 0.2rem 0.8rem;
+ width: calc(100% - 8rem);
+ font-size: 1.3rem;
+ border: 1px solid ${({ theme }) => theme.color.border};
+ border-radius: ${({ theme }) => theme.borderRadius.primary};
+`;
+
+export const ContentWrapper = styled.section`
+ width: 100%;
+`;
+
+export const Content = styled.textarea`
+ resize: none;
+ margin: 0.5rem 0;
+ padding: 1rem;
+ height: 55vh;
+ width: 100%;
+ border: 1px solid ${({ theme }) => theme.color.border};
+ border-radius: ${({ theme }) => theme.borderRadius.primary};
+ font-size: 1rem;
+`;
+
+export const InquiryFileWrapper = styled.div`
+ display: flex;
+ height: 40px;
+`;
+
+export const InquiryFileLabel = styled.label`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+ font-size: 1rem;
+ width: 6rem;
+ background: ${({ theme }) => theme.color.navy};
+ color: ${({ theme }) => theme.color.white};
+ border: 1px solid ${({ theme }) => theme.color.navy};
+ border-radius: ${({ theme }) => theme.borderRadius.primary} 0 0
+ ${({ theme }) => theme.borderRadius.primary};
+
+ &:hover {
+ background: ${({ theme }) => theme.color.lightgrey};
+ color: ${({ theme }) => theme.color.navy};
+ border: 1px solid ${({ theme }) => theme.color.navy};
+ transition: all 0.3s ease-in-out;
+ }
+`;
+
+export const InquiryShowFile = styled.span`
+ display: flex;
+ justify-content: start;
+ align-items: center;
+ padding: 0.5rem;
+ border: 1px solid ${({ theme }) => theme.color.border};
+ width: 40%;
+ color: ${({ theme }) => theme.color.navy};
+ border-radius: 0 ${({ theme }) => theme.borderRadius.primary}
+ ${({ theme }) => theme.borderRadius.primary} 0;
+`;
+
+export const InquiryFile = styled.input`
+ position: absolute;
+ width: 0;
+ height: 0;
+ overflow: hidden;
+`;
+
+export const FileImg = styled.img`
+ margin-left: 0.5rem;
+ width: 60px;
+ height: 40px;
+`;
+
+export const SendButtonWrapper = styled.div`
+ width: 100%;
+ display: flex;
+ justify-content: end;
+`;
+
+export const SendButton = styled.button`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 1rem;
+ width: 6rem;
+ background: ${({ theme }) => theme.color.navy};
+ border-radius: ${({ theme }) => theme.borderRadius.large};
+ color: ${({ theme }) => theme.color.white};
+ border: 1px solid ${({ theme }) => theme.color.navy};
+ padding: 0.5em;
+
+ &:hover {
+ background: ${({ theme }) => theme.color.lightgrey};
+ color: ${({ theme }) => theme.color.navy};
+ border: 1px solid ${({ theme }) => theme.color.navy};
+ transition: all 0.3s ease-in-out;
+ }
+`;
diff --git a/src/pages/customerService/inquiry/Inquiry.tsx b/src/pages/customerService/inquiry/Inquiry.tsx
new file mode 100644
index 00000000..6fc9322e
--- /dev/null
+++ b/src/pages/customerService/inquiry/Inquiry.tsx
@@ -0,0 +1,175 @@
+import { Fragment } from 'react/jsx-runtime';
+import {
+ INQUIRY_CATEGORY,
+ INQUIRY_MESSAGE,
+} from '../../../constants/customerService';
+import * as S from './Inquiry.styled';
+import { ChevronDownIcon } from '@heroicons/react/24/outline';
+import React, { useEffect, useState } from 'react';
+import type { InquiryFormData } from '../../../models/inquiry';
+import { usePostInquiry } from '../../../hooks/usePostInquiry';
+
+interface FormStateType {
+ category: string;
+ title: string;
+ content: string;
+ fileValue: string;
+ fileImage: string | null;
+}
+
+export default function Inquiry() {
+ const { mutate: postInquiry } = usePostInquiry();
+ const [isOpen, setIsOpen] = useState(false);
+ const [form, setForm] = useState({
+ category: INQUIRY_MESSAGE.categoryDefault,
+ title: '',
+ content: '',
+ fileValue: INQUIRY_MESSAGE.fileDefault,
+ fileImage: null,
+ });
+
+ const handleSubmitInquiry = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const formData = new FormData(e.currentTarget as HTMLFormElement);
+
+ const formDataObj: InquiryFormData = {
+ category: formData.get('category') as string,
+ title: formData.get('title') as string,
+ content: formData.get('content') as string,
+ };
+
+ const data = new Blob([JSON.stringify(formDataObj)], {
+ type: 'application/json',
+ });
+
+ formData.append('inquiryDto', data);
+
+ // 모달처리하기
+ let isValid = true;
+
+ Array.from(formData.entries()).forEach(([key, value]) => {
+ if (key === 'category' && value === INQUIRY_MESSAGE.categoryDefault)
+ return (isValid = false);
+ if (key === 'title' && value === '') return (isValid = false);
+ if (key === 'content' && value === '') return (isValid = false);
+ });
+
+ if (isValid) {
+ postInquiry(formData);
+ setForm({
+ category: INQUIRY_MESSAGE.categoryDefault,
+ title: '',
+ content: '',
+ fileValue: INQUIRY_MESSAGE.fileDefault,
+ fileImage: null,
+ });
+ }
+ };
+
+ const handleClickCategoryValue = (category: string) => {
+ setForm((prev) => ({ ...prev, category }));
+ setIsOpen((prev) => !prev);
+ };
+ const handleChangeFile = (e: React.ChangeEvent) => {
+ const fileValue = e.target.value;
+ const image = e.target.files?.[0];
+
+ // 파일 크기 제한 (예: 5MB)
+ const MAX_FILE_SIZE = 5 * 1024 * 1024;
+ if (image && image.size > MAX_FILE_SIZE) {
+ alert('파일 크기는 5MB 이하만 가능합니다.');
+ e.target.value = '';
+ return;
+ }
+
+ const fileImage = image ? URL.createObjectURL(image) : null;
+ setForm((prev) => ({ ...prev, fileValue, fileImage }));
+ };
+
+ useEffect(() => {
+ return () => {
+ if (form.fileImage) {
+ URL.revokeObjectURL(form.fileImage);
+ }
+ };
+ }, [form.fileImage]);
+
+ return (
+
+
+ DevPals 문의하기
+
+
+
+
+
+ setIsOpen((prev) => !prev)}
+ $isOpen={isOpen}
+ >
+ {form.category}
+
+
+ {isOpen && (
+
+ {INQUIRY_CATEGORY.map((category) => (
+
+ handleClickCategoryValue(category.title)}
+ >
+ {category.title}
+
+
+ ))}
+
+ )}
+
+
+ setForm((prev) => ({ ...prev, title: e.target.value }))
+ }
+ />
+
+
+
+ setForm((prev) => ({ ...prev, content: e.target.value }))
+ }
+ >
+
+ 파일찾기
+ {form.fileValue}
+ handleChangeFile(e)}
+ />
+ {form.fileImage && }
+
+
+
+ 제출
+
+
+
+
+ );
+}
diff --git a/src/components/common/customerService/notice/Notice.styled.ts b/src/pages/customerService/notice/Notice.styled.ts
similarity index 100%
rename from src/components/common/customerService/notice/Notice.styled.ts
rename to src/pages/customerService/notice/Notice.styled.ts
diff --git a/src/components/common/customerService/notice/Notice.tsx b/src/pages/customerService/notice/Notice.tsx
similarity index 100%
rename from src/components/common/customerService/notice/Notice.tsx
rename to src/pages/customerService/notice/Notice.tsx
diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx
index f4ea88fd..c4f80484 100644
--- a/src/routes/AppRoutes.tsx
+++ b/src/routes/AppRoutes.tsx
@@ -19,10 +19,9 @@ const ChangePassword = lazy(
const Main = lazy(() => import('../pages/main/Index'));
const Layout = lazy(() => import('../components/common/layout/Layout'));
const Home = lazy(() => import('../pages/home/Home'));
-const FAQ = lazy(() => import('../components/common/customerService/faq/FAQ'));
-const Notice = lazy(
- () => import('../components/common/customerService/notice/Notice')
-);
+const FAQ = lazy(() => import('../pages/customerService/faq/FAQ'));
+const Notice = lazy(() => import('../pages/customerService/notice/Notice'));
+const Inquiry = lazy(() => import('../pages/customerService/inquiry/Inquiry'));
const MyPage = lazy(() => import('../pages/mypage/MyPage'));
const UserPage = lazy(() => import('../pages/userpage/UserPage'));
const Apply = lazy(() => import('../pages/apply/ApplyStep'));
@@ -132,6 +131,18 @@ const AppRoutes = () => {
),
},
+ {
+ path: ROUTES.inquiry,
+ element: (
+
+
+
+
+
+
+
+ ),
+ },
{
path: ROUTES.createProject,
element: (