Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions src/api/inquiry.api.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { ApiCommonBasicType } from '../models/apiCommon';
import { httpClient } from './http.api';

export const postInquiry = async (formData: FormData) => {
try {
const response = await httpClient.post('/inquiry', formData, {
await httpClient.post<ApiCommonBasicType>('/inquiry', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
console.log(response);
} catch (e) {
console.error('문의하기 에러', e);
throw e;
Expand Down
14 changes: 8 additions & 6 deletions src/components/common/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as S from './Header.styled';
import Mainlogo from '../../../assets/mainlogo.svg';
import { Link } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';
import DropDown from '../dropDown/DropDown';
import Avatar from '../avatar/Avatar';
import { useAuth } from '../../../hooks/useAuth';
Expand All @@ -16,20 +16,22 @@ import { formatImgPath } from '../../../util/formatImgPath';
import bell from '../../../assets/bell.svg';
import Notification from './Notification/Notification';
import bellLogined from '../../../assets/bellLogined.svg';
// import useNotification from '../../../hooks/useNotification';
import useNotification from '../../../hooks/useNotification';
import { useEffect } from 'react';
import { testLiveAlarm } from '../../../api/alarm.api';
Comment on lines +19 to 21
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

불필요한 import는 제거하거나 실제 로직으로 복구해 주세요.

useNotification, useEffect, testLiveAlarm 가 모두 사용처 없이 import​만 존재합니다. 빌드-타임에 ESLint 오류가 발생하며 번들 크기도 불필요하게 증가합니다. 실제 알림 기능을 되살릴 계획이 없다면 import 와 주석 처리된 코드를 함께 제거해 주세요.

-import useNotification from '../../../hooks/useNotification';
-import { useEffect } from 'react';
-import { testLiveAlarm } from '../../../api/alarm.api';
+// 알림 기능이 복구될 때까지 보류 – 필요 시 다시 추가
📝 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
import useNotification from '../../../hooks/useNotification';
import { useEffect } from 'react';
import { testLiveAlarm } from '../../../api/alarm.api';
// 알림 기능이 복구될 때까지 보류 – 필요 시 다시 추가
🧰 Tools
🪛 ESLint

[error] 19-19: 'useNotification' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 20-20: 'useEffect' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 21-21: 'testLiveAlarm' is defined but never used.

(@typescript-eslint/no-unused-vars)


function Header() {
const location = useLocation();
const { isOpen, message, handleModalOpen, handleModalClose } = useModal();
const { userLogout } = useAuth(handleModalOpen);
const isLoggedIn = useAuthStore((state) => state.isLoggedIn);
const { myData, isLoading } = useMyProfileInfo();

// const { signalData, setSignalData } = useNotification();

useEffect(() => {
testLiveAlarm();
}, []);
// useEffect(() => {
// testLiveAlarm();
// }, []);

const profileImg = myData?.profileImg
? `${import.meta.env.VITE_APP_IMAGE_CDN_URL}/${formatImgPath(
Expand Down Expand Up @@ -92,7 +94,7 @@ function Header() {
<Link to={ROUTES.manageProjectsRoot}>
<S.Item>공고관리</S.Item>
</Link>
<Link to={ROUTES.inquiry}>
<Link to={ROUTES.inquiry} state={{ from: location.pathname }}>
<S.Item>문의하기</S.Item>
</Link>
Comment on lines +97 to 99
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

location.state 가 없는 경우를 대비한 fallback 처리 필요

Link 컴포넌트의 state={{ from: location.pathname }} 은 정상 흐름에서 유용하지만,
Inquiry 페이지를 직접 URL 로 진입했을 때는 location.stateundefined가 됩니다.
해당 경로는 훅 usePostInquiry 에서 그대로 사용되므로 런타임 오류가 발생할 수 있습니다.

-<Link to={ROUTES.inquiry} state={{ from: location.pathname }}>
+<Link
+  to={ROUTES.inquiry}
+  state={{ from: location.pathname || ROUTES.main }}  // fallback
+>

🏁 Script executed:

#!/bin/bash
# usePostInquiry 훅 사용 위치 확인
echo "=== usePostInquiry 사용 확인 ==="
rg "usePostInquiry" -A5

# Inquiry 페이지에서 location.state 사용 확인
echo "=== location.state 사용 위치 확인 ==="
rg "location.state" -A5

# useLocation 훅 사용 위치 확인
echo "=== useLocation 사용 확인 ==="
rg "useLocation" -A5

Length of output: 7546


Inquiry 페이지에서 location.state.from에 대한 안전한 fallback 처리 필요

직접 URL로 진입할 경우 location.stateundefined가 되어 usePostInquiry(location.state.from, …) 호출 시 런타임 에러가 발생합니다. Header나 다른 Link 컴포넌트의 state 설정만으로는 직접 진입 시 보호할 수 없으므로, Inquiry 페이지에서 소비자 쪽(fallback) 처리를 적용해야 합니다.

  • src/pages/customerService/inquiry/Inquiry.tsx
    1. import { ROUTES } from '../../../constants/routes'; 추가
    2. usePostInquiry 호출부를 optional chaining과 nullish coalescing으로 변경
+ import { ROUTES } from '../../../constants/routes';
...
- const { mutate: postInquiry } = usePostInquiry(
-   location.state.from,
-   handleModalOpen
- );
+ const { mutate: postInquiry } = usePostInquiry(
+   location.state?.from ?? ROUTES.main,
+   handleModalOpen
+ );

<Link to='#' onClick={(e) => e.preventDefault()}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from 'react';
import type { MyInquiries } from '../../../../../models/activityLog';
import * as S from './Inquiry.styled';
import { INQUIRY_MESSAGE } from '../../../../../constants/customerService';
import { My_INQUIRIES_MESSAGE } from '../../../../../constants/customerService';

interface InquiryProps {
list: MyInquiries;
Expand Down Expand Up @@ -49,7 +49,7 @@ export default function Inquiry({ list, no }: InquiryProps) {
))}
</S.InquiryImgWrapper>
<S.MessageWrapper>
{INQUIRY_MESSAGE.blowUpMessage}
{My_INQUIRIES_MESSAGE.blowUpMessage}
</S.MessageWrapper>
</S.InquiryImgContainer>
)}
Expand All @@ -64,7 +64,7 @@ export default function Inquiry({ list, no }: InquiryProps) {
}
>
<S.ModalImgMessageWrapper>
{INQUIRY_MESSAGE.isImageOpenMessage}
{My_INQUIRIES_MESSAGE.isImageOpenMessage}
</S.ModalImgMessageWrapper>
<S.ModalImg src={isImageOpen.url} />
</S.ModalImgWrapper>
Expand Down
11 changes: 10 additions & 1 deletion src/constants/customerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,18 @@ export const INQUIRY_CATEGORY = [
export const EMPTY_IMAGE =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' as const;

export const INQUIRY_MESSAGE = {
export const My_INQUIRIES_MESSAGE = {
categoryDefault: '카테고리',
fileDefault: '선택된 파일이 없습니다.',
blowUpMessage: '클릭하면 이미지를 크게 볼 수 있습니다.',
isImageOpenMessage: '이미지를 클릭하면 사라집니다.',
};

export const INQUIRY_MESSAGE = {
selectCategory: '카테고리를 선택하세요.',
writeTitle: '제목을 적어주세요.',
writeContent: '내용을 적어주세요.',
inquiredSuccess: '문의글이 작성되었습니다.',
inquiredError: '문의글 작성에 실패하였습니다.',
validationFile: '파일 크기는 5MB 이하만 가능합니다.',
};
2 changes: 1 addition & 1 deletion src/hooks/useMyInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const useMyProfileInfo = () => {
const { data, isLoading } = useQuery<ApiUserInfo>({
queryKey: myInfoKey.myProfile,
queryFn: () => getMyInfo(),
staleTime: 1 * 60 * 1000,
staleTime: Infinity,
enabled: isLoggedIn,
});

Expand Down
27 changes: 25 additions & 2 deletions src/hooks/usePostInquiry.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
import { ActivityLog } from './queries/keys';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { postInquiry } from '../api/inquiry.api';
import { AxiosError } from 'axios';
import useAuthStore from '../store/authStore';
import { useNavigate } from 'react-router-dom';
import { INQUIRY_MESSAGE } from '../constants/customerService';

export const usePostInquiry = () => {
export const usePostInquiry = (
handleModalOpen: (message: string) => void,
pathname: string = ''
) => {
const userId = useAuthStore((state) => state.userData?.id);
const navigate = useNavigate();
const queryClient = useQueryClient();

const mutate = useMutation<void, AxiosError, FormData>({
mutationFn: (formData) => postInquiry(formData),
onSuccess: () => {
queryClient.invalidateQueries();
queryClient.invalidateQueries({
queryKey: [ActivityLog.myInquiries, userId],
});
setTimeout(() => {
if (pathname === '' || !pathname) {
navigate(-1);
} else {
navigate(pathname);
}
}, 1000);

handleModalOpen(INQUIRY_MESSAGE.inquiredSuccess);
},
onError: () => {
handleModalOpen(INQUIRY_MESSAGE.inquiredError);
},
});

Expand Down
4 changes: 4 additions & 0 deletions src/models/apiCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ export interface ApiCommonType {
message: string;
}

export interface ApiCommonBasicType extends ApiCommonType {
data: boolean;
}

export interface User {
id: number;
nickname: string;
Expand Down
9 changes: 8 additions & 1 deletion src/pages/customerService/MoveInquiredLink.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { useLocation } from 'react-router-dom';
import { ROUTES } from '../../constants/routes';
import * as S from './MoveInquiredLink.styled';

export default function MovedInquiredLink() {
return <S.MoveInquiredLink to={ROUTES.inquiry}>문의하기</S.MoveInquiredLink>;
const location = useLocation();

return (
<S.MoveInquiredLink to={ROUTES.inquiry} state={{ from: location.pathname }}>
문의하기
</S.MoveInquiredLink>
);
}
2 changes: 1 addition & 1 deletion src/pages/customerService/inquiry/Inquiry.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const CategoryWrapper = styled.div`
position: relative;
`;

export const CategorySelect = styled.button<{ $isOpen: boolean }>`
export const CategorySelect = styled.button<{ $isCategoryOpen: boolean }>`
padding: 0.3rem 0.5rem;
width: 9rem;
display: flex;
Expand Down
88 changes: 59 additions & 29 deletions src/pages/customerService/inquiry/Inquiry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import { Fragment } from 'react/jsx-runtime';
import {
INQUIRY_CATEGORY,
INQUIRY_MESSAGE,
My_INQUIRIES_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';
import { useLocation } from 'react-router-dom';
import { useModal } from '../../../hooks/useModal';
import Modal from '../../../components/common/modal/Modal';

interface FormStateType {
category: string;
Expand All @@ -18,13 +22,23 @@ interface FormStateType {
}

export default function Inquiry() {
const { mutate: postInquiry } = usePostInquiry();
const [isOpen, setIsOpen] = useState<boolean>(false);
const location = useLocation();
const {
isOpen: isModalOpen,
message,
handleModalOpen,
handleModalClose,
} = useModal();
const { mutate: postInquiry } = usePostInquiry(
handleModalOpen,
location.state.from || ''
);
const [isCategoryOpen, setIsCategoryOpen] = useState<boolean>(false);
const [form, setForm] = useState<FormStateType>({
category: INQUIRY_MESSAGE.categoryDefault,
category: My_INQUIRIES_MESSAGE.categoryDefault,
title: '',
content: '',
fileValue: INQUIRY_MESSAGE.fileDefault,
fileValue: My_INQUIRIES_MESSAGE.fileDefault,
fileImage: null,
});

Expand All @@ -45,40 +59,43 @@ export default function Inquiry() {

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);
});
const isValid = {
category: form.category !== My_INQUIRIES_MESSAGE.categoryDefault,
title: form.title.trim() !== '',
content: form.content.trim() !== '',
};

if (isValid) {
postInquiry(formData);
setForm({
category: INQUIRY_MESSAGE.categoryDefault,
title: '',
content: '',
fileValue: INQUIRY_MESSAGE.fileDefault,
fileImage: null,
});
if (!isValid.category) {
return handleModalOpen(INQUIRY_MESSAGE.selectCategory);
}
if (!isValid.title) {
return handleModalOpen(INQUIRY_MESSAGE.writeTitle);
}
if (!isValid.content) {
return handleModalOpen(INQUIRY_MESSAGE.writeContent);
}

postInquiry(formData);
setForm({
category: My_INQUIRIES_MESSAGE.categoryDefault,
title: '',
content: '',
fileValue: My_INQUIRIES_MESSAGE.fileDefault,
fileImage: null,
});
};

const handleClickCategoryValue = (category: string) => {
setForm((prev) => ({ ...prev, category }));
setIsOpen((prev) => !prev);
setIsCategoryOpen((prev) => !prev);
};
const handleChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => {
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 이하만 가능합니다.');
handleModalOpen(INQUIRY_MESSAGE.validationFile);
e.target.value = '';
return;
}
Expand Down Expand Up @@ -109,8 +126,12 @@ export default function Inquiry() {
<S.Nav>
<S.CategoryWrapper>
<S.CategorySelect
onClick={() => setIsOpen((prev) => !prev)}
$isOpen={isOpen}
type='button'
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setIsCategoryOpen((prev) => !prev);
}}
$isCategoryOpen={isCategoryOpen}
>
{form.category} <ChevronDownIcon />
<S.CategoryValueInput
Expand All @@ -119,11 +140,12 @@ export default function Inquiry() {
value={form.category}
/>
</S.CategorySelect>
{isOpen && (
{isCategoryOpen && (
<S.CategoryButtonWrapper>
{INQUIRY_CATEGORY.map((category) => (
<Fragment key={category.title}>
<S.CategoryButton
type='button'
onClick={() => handleClickCategoryValue(category.title)}
>
{category.title}
Expand Down Expand Up @@ -166,10 +188,18 @@ export default function Inquiry() {
</S.InquiryFileWrapper>
</S.ContentWrapper>
<S.SendButtonWrapper>
<S.SendButton type='submit'>제출</S.SendButton>
<S.SendButton
type='submit'
onClick={() => setIsCategoryOpen((prev) => !prev)}
>
제출
</S.SendButton>
</S.SendButtonWrapper>
</S.InquiryWrapper>
</S.InquiryForm>
<Modal isOpen={isModalOpen} onClose={handleModalClose}>
{message}
</Modal>
</S.Container>
);
}