Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
10 changes: 0 additions & 10 deletions src/api/activityLog.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,3 @@ export const getMyInquiries = async () => {
throw e;
}
};

export const getAllInquiries = async () => {
try {
const response = await httpClient.get<ApiAllInquiries>(`/inquiry`);
return response.data.data;
} catch (e) {
console.error('전체 문의 조회 에러', e);
throw e;
}
};
61 changes: 61 additions & 0 deletions src/api/admin/customerService/Inquiry.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { ApiCommonBasicType } from '../../../models/apiCommon';
import type {
ApiAdminInquiry,
ApiAdminInquiryDetail,
InquiryAnswerBody,
} from '../../../models/inquiry';
import { httpClient } from '../../http.api';

export const getAllInquiries = async () => {
try {
const response = await httpClient.get<ApiAdminInquiry>(`/inquiry`);
return response.data.data;
} catch (e) {
console.error('전체 문의 조회 에러', e);
throw e;
}
};

export const getInquiryDetail = async (id: string) => {
try {
const response = await httpClient.get<ApiAdminInquiryDetail>(
`/inquiry/${id}`
);

return response.data.data;
} catch (e) {
console.error(e);
throw e;
}
};

export const postInquiryAnswer = async ({ id, answer }: InquiryAnswerBody) => {
try {
await httpClient.post<ApiCommonBasicType>(`/inquiry/${id}/answer`, {
answer,
});
} catch (e) {
console.error(e);
throw e;
}
};

export const patchInquiryAnswer = async ({ id, answer }: InquiryAnswerBody) => {
try {
await httpClient.patch<ApiCommonBasicType>(`/inquiry/${id}/answer`, {
answer,
});
} catch (e) {
console.error(e);
throw e;
}
};

export const deleteInquiry = async (id: string) => {
try {
await httpClient.delete<ApiCommonBasicType>(`/inquiry/${id}`);
} catch (e) {
console.error(e);
throw e;
}
};
41 changes: 41 additions & 0 deletions src/components/admin/adminInquiry/AdminInquiry.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Link } from 'react-router-dom';
import styled, { css } from 'styled-components';

export const AdminInquiryContainer = styled(Link)``;

export const AdminInquiryWrapper = styled.div`
padding: 0.8rem;
display: grid;
grid-template-columns: 13% 51% 15% 11% 10%;
justify-content: center;
align-items: center;
font-size: 1.1rem;
`;

export const AdminInquiryTitle = styled.div``;

export const AdminInquiryCategory = styled.span`
font-size: 1.1rem;
font-weight: 500;
`;

export const AdminInquiryUser = styled.div``;

export const AdminInquiryDate = styled.span`
font-size: 0.9rem;
color: ${({ theme }) => theme.color.deepGrey};
`;

export const AdminInquiryState = styled.div<{ $hasAnswer: boolean }>`
text-align: center;
color: ${({ $hasAnswer, theme }) =>
$hasAnswer ? theme.color.white : theme.color.green};
padding: 0.3rem;

${({ $hasAnswer }) =>
$hasAnswer &&
css`
border-radius: ${({ theme }) => theme.borderRadius.large};
background: ${({ theme }) => theme.color.navy};
`}
`;
25 changes: 25 additions & 0 deletions src/components/admin/adminInquiry/AdminInquiry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ADMIN_ROUTE } from '../../../constants/routes';
import type { AdminInquiry as TAdminInquiry } from '../../../models/inquiry';
import ContentBorder from '../../common/contentBorder/ContentBorder';
import * as S from './AdminInquiry.styled';

interface AdminInquiryProps {
list: TAdminInquiry;
}

export default function AdminInquiry({ list }: AdminInquiryProps) {
return (
<S.AdminInquiryContainer to={`${ADMIN_ROUTE.detail}/${list.id}`}>
<S.AdminInquiryWrapper>
<S.AdminInquiryCategory>[{list.category}]</S.AdminInquiryCategory>
<S.AdminInquiryTitle>{list.title}</S.AdminInquiryTitle>
<S.AdminInquiryUser>{list.user.nickname}</S.AdminInquiryUser>
<S.AdminInquiryDate>{list.createdAt}</S.AdminInquiryDate>
<S.AdminInquiryState $hasAnswer={list.state}>
{list.state ? '답변완료' : '확인중'}
</S.AdminInquiryState>
</S.AdminInquiryWrapper>
<ContentBorder />
</S.AdminInquiryContainer>
);
}
34 changes: 34 additions & 0 deletions src/components/admin/adminInquiry/AdminInquiryAnswer.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import styled from 'styled-components';
import {
AdminInquiryContentContainer,
AdminInquiryContentWrapper,
InquiryContent,
InquiryHeaderWrapper,
InquiryInfo,
} from './AdminInquiryDetailContent.styled';
import { SendButton } from '../../user/customerService/inquiry/Inquiry.styled';

export const InquiryAnswerContentContainer = styled(
AdminInquiryContentContainer
)``;

export const InquiryAnswerContentWrapper = styled(AdminInquiryContentWrapper)``;

export const AnswerHeaderWrapper = styled(InquiryHeaderWrapper)`
display: flex;
gap: 3rem;
`;

export const InquiryAnswerInfo = styled(InquiryInfo)`
display: flex;
align-items: center;
`;

export const InquiryAnswerButton = styled(SendButton)`
font-size: 0.9rem;
padding: 0.3rem 0.5rem;
`;

export const AnswerButtonSpan = styled.span``;

export const InquiryAnswerContent = styled(InquiryContent)``;
46 changes: 46 additions & 0 deletions src/components/admin/adminInquiry/AdminInquiryAnswer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useEffect, useState } from 'react';
import * as S from './AdminInquiryAnswer.styled';
import { Link, useOutletContext } from 'react-router-dom';
import { ADMIN_ROUTE } from '../../../constants/routes';

interface AdminInquiryDetailContentOutletContext {
createdAt: string;
answerData: string;
}

type LinkType = '작성하기' | '수정하기';

export default function AdminInquiryAnswer() {
const { createdAt, answerData }: AdminInquiryDetailContentOutletContext =
useOutletContext();

const [answer, setAnswer] = useState<string>('');
const selectButton: LinkType = answer === null ? '작성하기' : '수정하기';
Comment on lines +17 to +18
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

버튼 선택 로직의 타입 불일치를 수정하세요.

answer 상태가 string으로 타입이 지정되어 있는데 null과 비교하고 있습니다. answerData가 없는 경우를 올바르게 처리해야 합니다.

-  const [answer, setAnswer] = useState<string>('');
-  const selectButton: LinkType = answer === null ? '작성하기' : '수정하기';
+  const [answer, setAnswer] = useState<string | null>(null);
+  const selectButton: LinkType = !answer || answer.trim() === '' ? '작성하기' : '수정하기';
🤖 Prompt for AI Agents
In src/components/admin/adminInquiry/AdminInquiryAnswer.tsx around lines 16 to
17, the code compares the string-typed state variable 'answer' to null, which is
a type mismatch. To fix this, adjust the condition to check if 'answer' is an
empty string instead of null, or modify the state type to include null if that
is intended. Ensure the button selection logic correctly reflects whether
'answer' has meaningful content by using a proper check like answer === '' or
answer.length === 0.


useEffect(() => {
setAnswer(answerData);
}, [answerData]);

return (
<S.InquiryAnswerContentContainer>
<S.InquiryAnswerContentWrapper>
<S.AnswerHeaderWrapper>
<S.InquiryAnswerInfo>
{createdAt ? createdAt : ''}
</S.InquiryAnswerInfo>
<S.InquiryAnswerButton
as={Link}
to={
selectButton === '작성하기'
? ADMIN_ROUTE.write
: ADMIN_ROUTE.modification
}
>
<S.AnswerButtonSpan>{selectButton}</S.AnswerButtonSpan>
</S.InquiryAnswerButton>
</S.AnswerHeaderWrapper>
<S.InquiryAnswerContent>{answer}</S.InquiryAnswerContent>
</S.InquiryAnswerContentWrapper>
</S.InquiryAnswerContentContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import styled from 'styled-components';
import {
AdminInquiryContentContainer,
AdminInquiryContentWrapper,
InquiryContent,
} from './AdminInquiryDetailContent.styled';
import {
AnswerHeaderWrapper,
InquiryAnswerButton,
InquiryAnswerInfo,
} from './AdminInquiryAnswer.styled';

export const InquiryAnswerContentContainer = styled(
AdminInquiryContentContainer
)``;

export const InquiryAnswerWriteWrapper = styled(AdminInquiryContentWrapper)``;

export const InquiryAnswerWriteHeaderWrapper = styled(AnswerHeaderWrapper)`
display: flex;
gap: 3rem;
`;

export const InquiryAnswerWriteInfo = styled(InquiryAnswerInfo)`
display: flex;
align-items: center;
`;

export const InquiryAnswerWriteButton = styled(InquiryAnswerButton)`
align-items: start;
height: fit-content;
font-size: 0.9rem;
padding: 0.3rem 0.5rem;
`;

export const InquiryAnswerWriteButtonSpan = styled.span``;

export const InquiryAnswerWrite = styled(InquiryContent)`
resize: none;
border: 1px solid ${({ theme }) => theme.color.placeholder};
border-radius: ${({ theme }) => theme.borderRadius.primary};
padding: 1rem;
`;
100 changes: 100 additions & 0 deletions src/components/admin/adminInquiry/AdminInquiryAnswerWrite.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { useOutletContext } from 'react-router-dom';
import * as S from './AdminInquiryAnswerWrite.styled';
import React, { useEffect, useRef, useState } from 'react';
import { InquiryAnswerBody } from '../../../models/inquiry';
import { UseMutationResult } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import Modal from '../../common/modal/Modal';
import { useModal } from '../../../hooks/useModal';
import { INQUIRY_MESSAGE } from '../../../constants/user/customerService';
import Spinner from '../../user/mypage/Spinner';

interface AdminInquiryAnswerWriteProps {
answerData: string;
isWrite: boolean;
id: string;
postInquiryAnswerMutate: UseMutationResult<
void,
AxiosError,
InquiryAnswerBody
>;
patchInquiryAnswerMutate: UseMutationResult<
void,
AxiosError,
InquiryAnswerBody
>;
}

export default function AdminInquiryAnswerWrite() {
const {
answerData,
isWrite,
id,
postInquiryAnswerMutate,
patchInquiryAnswerMutate,
} = useOutletContext<AdminInquiryAnswerWriteProps>();
const [answer, setAnswer] = useState<string>('');
const inputRef = useRef<HTMLTextAreaElement>(null);
const { isOpen, message, handleModalOpen, handleModalClose } = useModal();
const isLoading =
postInquiryAnswerMutate.isPending || patchInquiryAnswerMutate.isPending;

useEffect(() => {
if (answerData) {
setAnswer(answerData);
}
}, [answerData]);

if (isLoading) {
<Spinner />;
}

const handleSubmitAnswer = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

if (answer.trim() === '') {
return handleModalOpen(INQUIRY_MESSAGE.writeContent);
}

if (isWrite) {
postInquiryAnswerMutate.mutate({ id, answer: answer });
} else {
patchInquiryAnswerMutate.mutate({ id, answer: answer });
}
};

const handleChangeAnswer = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (inputRef && inputRef.current) {
inputRef.current.style.height = 'auto';
inputRef.current.style.height = `${inputRef.current.scrollHeight}px`;
}

const answerValue = e.target.value;

setAnswer(answerValue);
};

return (
<S.InquiryAnswerContentContainer>
<S.InquiryAnswerWriteWrapper as='form' onSubmit={handleSubmitAnswer}>
<S.InquiryAnswerWriteHeaderWrapper>
<S.InquiryAnswerWriteButton type='submit'>
<S.InquiryAnswerWriteButtonSpan>
답변하기
</S.InquiryAnswerWriteButtonSpan>
</S.InquiryAnswerWriteButton>
<S.InquiryAnswerWriteInfo></S.InquiryAnswerWriteInfo>
</S.InquiryAnswerWriteHeaderWrapper>
<S.InquiryAnswerWrite
as='textarea'
value={answer}
ref={inputRef}
onChange={handleChangeAnswer}
></S.InquiryAnswerWrite>
</S.InquiryAnswerWriteWrapper>
<Modal isOpen={isOpen} onClose={handleModalClose}>
{message}
</Modal>
</S.InquiryAnswerContentContainer>
);
}
12 changes: 12 additions & 0 deletions src/components/admin/adminInquiry/AdminInquiryDetail.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import styled from 'styled-components';
import { SpinnerWrapperStyled } from '../../user/mypage/Spinner.styled';

export const SpinnerWrapper = styled(SpinnerWrapperStyled)``;

export const AdminInquiryDetailContainer = styled.main`
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
gap: 1.5rem;
`;
Loading