Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d81af61
feat : 게시글 신고기능 구현
wldnjs990 Mar 4, 2025
2537f16
feat : 카테고리 전체 선택 안되는 오류 수정 + 답장 전송시 도착시간 1시간으로 텍스트 고정
wldnjs990 Mar 5, 2025
78810fb
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
wldnjs990 Mar 5, 2025
26ab9c0
feat : getPrevLetter api 엔드포인트 변경
wldnjs990 Mar 5, 2025
cb7053b
feat : 디테일 페이지 답장버튼 분기처리
wldnjs990 Mar 5, 2025
ebb37c5
feat : 편지상세페이지 zipCode바인딩
wldnjs990 Mar 6, 2025
83d6c94
refactor : 편지상세 페이지 컴포넌트 분리
wldnjs990 Mar 6, 2025
c923aa3
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
wldnjs990 Mar 6, 2025
eb8581c
feat : 편지 상세 컴포넌트 추가 분리 + 편지 평가 기능 구현 완료
wldnjs990 Mar 6, 2025
69ffab2
refactor : 신고모달 타입에서 null 제거 + 이전편지 가져오기, 타입 조금 수정
wldnjs990 Mar 6, 2025
8469d33
feat : 랜덤편지 편지 없을 경우 예외처리 UI 추가
wldnjs990 Mar 7, 2025
f7bdc3f
design : 편지작성, 편지상세 resize속성 제거
wldnjs990 Mar 7, 2025
88f1533
feat : 랜덤편지 데이터가 없을시 예외처리 UI 추가 + 쿨타임 상태일때 예외처리 UI 수정
wldnjs990 Mar 7, 2025
749fea4
chore : 랜덤편지 api console 제거
wldnjs990 Mar 7, 2025
bfc0be3
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
wldnjs990 Mar 7, 2025
bcf7853
feat : 임시저장 api 생성(연결 테스트 아직 안함)
wldnjs990 Mar 7, 2025
a00ebf5
feat : 편지 작성 페이지 임시저장 버튼 구현
wldnjs990 Mar 7, 2025
67633bc
feat : 편지 임시저장 80% 구현(승연님 작업 이후 임시저장 업데이트 분기 나눠야함)
wldnjs990 Mar 7, 2025
75d35f7
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
wldnjs990 Mar 8, 2025
25e4378
feat : 임시저장 최초답장 예외처리
wldnjs990 Mar 8, 2025
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/apis/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ client.interceptors.response.use(
}
}
}
if (isLoggedIn) logout();
console.error('Failed to refresh token', error);
// if (isLoggedIn) logout();
// console.error('Failed to refresh token', error);
Copy link
Collaborator

Choose a reason for hiding this comment

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

이건 임시로 주석 처리하신 건가용?

return Promise.reject(error);
},
);
Expand Down
15 changes: 14 additions & 1 deletion src/apis/letterDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,17 @@ const deleteLetter = async (letterId: string) => {
}
};

export { getLetter, deleteLetter };
const postEvaluateLetter = async (letterId: number, evaluation: LetterEvaluation) => {
try {
const res = await client.post(`/api/letters/${letterId}/evaluate`, {
evaluation: evaluation,
});
if (!res) throw new Error('편지 삭제 요청 도중 에러가 발생했습니다.');
console.log(res);
return res;
Copy link
Collaborator

Choose a reason for hiding this comment

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

res.status로 에러 체크하는 게 어떨까요!

} catch (error) {
console.error(error);
Copy link
Collaborator

Choose a reason for hiding this comment

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

throw error 로 에러를 다시 던져주면 좋을 것 같아요 :)))

}
};

export { getLetter, deleteLetter, postEvaluateLetter };
6 changes: 0 additions & 6 deletions src/apis/randomLetter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const getRandomLetters = async (category: string | null) => {
try {
const res = await client.get(`/api/random-letters/${category}`);
if (!res) throw new Error('랜덤 편지 데이터를 가져오는 도중 에러가 발생했습니다.');
console.log(res);
return res;
} catch (error) {
console.error(error);
Expand All @@ -13,8 +12,6 @@ const getRandomLetters = async (category: string | null) => {

const postRandomLettersApprove = async (approveRequest: ApproveRequest, callBack?: () => void) => {
try {
console.log('엔드포인트 : /api/random-letters/approve');
console.log('request', approveRequest);
Copy link
Collaborator

Choose a reason for hiding this comment

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

필요없는 console.log 지워주셨네요!

const res = await client.post('/api/random-letters/approve', approveRequest);
if (!res) throw new Error('랜덤편지 매칭수락 도중 에러가 발생했습니다.');
if (callBack) callBack();
Expand All @@ -30,7 +27,6 @@ const getRandomLetterMatched = async (callBack?: () => void) => {
if (!res)
throw new Error('랜덤 편지 최종 매칭 시간 검증 데이터를 가자오는 도중 에러가 발생했습니다.');
if (callBack) callBack();
console.log(res);
return res;
} catch (error) {
console.error(error);
Expand All @@ -43,7 +39,6 @@ const getRandomLetterCoolTime = async (callBack?: () => void) => {
if (!res)
throw new Error('랜덤 편지 최종 매칭 시간 검증 데이터를 가자오는 도중 에러가 발생했습니다.');
if (callBack) callBack();
console.log(res);
return res;
} catch (error) {
console.error(error);
Expand All @@ -54,7 +49,6 @@ const deleteRandomLetterMatching = async () => {
try {
const res = await client.delete('/api/random-letters/matching/cancel');
if (!res) throw new Error('매칭 취소 도중 에러가 발생했습니다.');
console.log(res);
return res;
} catch (error) {
console.log(error);
Expand Down
35 changes: 28 additions & 7 deletions src/apis/write.ts
Copy link
Collaborator

Choose a reason for hiding this comment

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

빨리 데이터 전달 드릴게요!!! 🥹

Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,21 @@
import client from './client';

const postLetter = async (data: LetterRequest) => {
console.log('request', data);
try {
const res = await client.post('/api/letters', data);
if (!res) throw new Error('편지 전송과정중에서 오류가 발생했습니다.');
console.log(`api 주소 : /api/letters, 전송타입 : post`);
if (!res) throw new Error('편지 전송과정에서 오류가 발생했습니다.');
return res;
} catch (error) {
console.error(error);
}
};

const postFirstReply = async (data: FirstReplyRequest) => {
console.log('Firstrequest', data);
try {
const res = await client.post('/api/random-letters/matching', data);
if (!res) throw new Error('최초 답장 전송과정중에서 오류가 발생했습니다.');
console.log(`api 주소 : /api/random-letters/matching, 전송타입 : post`);
console.log(res);
if (!res) throw new Error('최초 답장 전송과정에서 오류가 발생했습니다.');
return res;
} catch (error) {
console.error(error);
Expand All @@ -27,11 +26,33 @@ const postFirstReply = async (data: FirstReplyRequest) => {
const getPrevLetter = async (letterId: string) => {
try {
const res = await client.get(`/api/letters/${letterId}/previous`);
console.log(res);
if (!res) throw new Error('이전편지를 불러오는중 오류가 발생했습니다.');
return res;
} catch (error) {
console.error(error);
}
};

export { postLetter, postFirstReply, getPrevLetter };
// 임시저장 최초 생성
const postTemporarySave = async (data: TemporaryRequest) => {
try {
const res = client.post(`/api/letters/temporary-save`, data);
if (!res) throw new Error('편지 임시저장과정에서 오류가 발생했습니다.');
return res;
} catch (error) {
console.error(error);
}
};

// 임시저장 수정
const PatchTemporarySave = async (data: TemporaryRequest) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

함수명은 카멜 케이스로 통일하기로 했으니 바꿔주시면 좋을 것 같아요!

try {
const res = client.post(`/api/letters/temporary-save`, data);
if (!res) throw new Error('편지 임시저장과정에서 오류가 발생했습니다.');
return res;
} catch (error) {
console.error(error);
}
};

export { postLetter, postFirstReply, getPrevLetter, postTemporarySave, PatchTemporarySave };
Copy link
Collaborator

Choose a reason for hiding this comment

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

저는 함수마다 앞에 export붙이곤 했는데 이게 더 깔끔해보이네요 😲

12 changes: 3 additions & 9 deletions src/components/ReportModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';

import { postReports } from '@/apis/admin';
Expand All @@ -8,7 +8,7 @@ import TextareaField from './TextareaField';

interface ReportModalProps {
reportType: ReportType;
letterId: number | null;
letterId: number;
onClose: () => void;
}

Expand Down Expand Up @@ -42,20 +42,14 @@ const ReportModal = ({ reportType, letterId, onClose }: ReportModalProps) => {
const res = await postReports(postReportRequest);
if (res?.status === 200) {
alert('신고 처리되었습니다.');
console.log(res);
onClose();
} else if (res?.status === 409) {
alert('신고한 이력이 있습니다.');
onClose();
}
};

useEffect(() => {
if (!postReportRequest.letterId) {
alert('신고 모달을 여는 과정에서 오류가 발생했습니다. 새로고침을 눌러주세요');
onClose();
}
});

return (
<ConfirmModal
title="신고 사유를 선택해주세요"
Expand Down
61 changes: 61 additions & 0 deletions src/pages/LetterDetail/components/DegreeSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { postEvaluateLetter } from '@/apis/letterDetail';
import { CloudIcon, SnowIcon, WarmIcon } from '@/assets/icons';

interface DegreeSelector {
letterDetail: LetterDetail | null;
setLetterDetail: React.Dispatch<React.SetStateAction<LetterDetail>>;
}
export default function DegreeSelector({ letterDetail, setLetterDetail }: DegreeSelector) {
const handlePostEvaluateLetter = async (
letterId: number | undefined,
evaluation: LetterEvaluation,
) => {
if (!letterId) return alert('편지id값이 담겨있지 않습니다.');
const res = await postEvaluateLetter(letterId, evaluation);
if (res?.status === 200) {
console.log('평가완료');
setLetterDetail((cur) => ({ ...cur, evaluated: true }));
}
};
const DEGREES = [
{
icon: <WarmIcon className="h-5 w-5" />,
title: '따뜻해요',
onClick: () => {
handlePostEvaluateLetter(letterDetail?.letterId, 'GOOD');
},
},
{
icon: <CloudIcon className="h-5 w-5" />,
title: '그럭저럭',
onClick: () => {
handlePostEvaluateLetter(letterDetail?.letterId, 'SOSO');
},
},
{
icon: <SnowIcon className="h-5 w-5" />,
title: '앗! 차가워',
onClick: () => {
handlePostEvaluateLetter(letterDetail?.letterId, 'BAD');
},
},
];
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기서는 onClick 함수 호출을 빼고 아래 return 다음의 onClick에서 호출하는 건 어떨까요?

return (
<div className="caption-b text-primary-1 bg-primary-5 absolute top-7 z-40 flex flex-col gap-1 p-2 shadow">
{DEGREES.map((degree, idx) => {
return (
<button
key={idx}
className="flex items-center justify-start gap-1"
onClick={() => {
degree.onClick();
}}
>
{degree.icon}
{degree.title}
</button>
);
})}
</div>
);
}
26 changes: 26 additions & 0 deletions src/pages/LetterDetail/components/LetterDetailContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { twMerge } from 'tailwind-merge';

import { FONT_TYPE_OBJ } from '@/pages/Write/constants';

interface LetterDetailContent {
letterDetail: LetterDetail;
}
export default function LetterDetailContent({ letterDetail }: LetterDetailContent) {
return (
<>
<div className="flex flex-col gap-3 px-5">
<span className="body-b mt-[55px]">TO. 따숨이</span>
<span className="body-sb">{letterDetail.title}</span>
</div>
<textarea
readOnly
value={letterDetail.content}
className={twMerge(
`body-r basic-theme min-h-full w-full grow resize-none px-6`,
letterDetail && FONT_TYPE_OBJ[letterDetail.fontType],
Copy link
Collaborator

Choose a reason for hiding this comment

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

letterDetail은 필수 prop으로 보이는데 letterDetail&& 로 체크하신 이유가 궁금합니다!

)}
></textarea>
<span className="body-sb mt-10 flex justify-end">FROM. {letterDetail.zipCode}</span>
</>
);
}
50 changes: 50 additions & 0 deletions src/pages/LetterDetail/components/LetterDetailDegreeButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useEffect, useRef } from 'react';

import { ThermostatIcon } from '@/assets/icons';

interface LetterDetailDegreeButton {
letterDetail: LetterDetail | null;
setDegreeModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
export default function LetterDetailDegreeButton({
letterDetail,
setDegreeModalOpen,
}: LetterDetailDegreeButton) {
const degreeButtonRef = useRef<HTMLButtonElement>(null);

useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
const target = event.target as Node;
if (!target || degreeButtonRef.current?.contains(target)) {
return;
}
setDegreeModalOpen(false);
};

document.body.addEventListener('click', handleOutsideClick);

return () => {
document.body.removeEventListener('click', handleOutsideClick);
};
}, [setDegreeModalOpen]);
return (
<>
{letterDetail?.evaluated ? (
<div>
<span className="caption-b text-primary-1">온도 측정된 편지에요!</span>
</div>
Copy link
Collaborator

Choose a reason for hiding this comment

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

spandiv 안에 한 번 더 넣으신 이유가 궁금합니당

) : (
<button
ref={degreeButtonRef}
className="flex items-center justify-center gap-1"
onClick={() => {
setDegreeModalOpen((cur) => !cur);
}}
>
<ThermostatIcon className="h-6 w-6" />
<span className="caption-b text-primary-1">편지 온도</span>
</button>
)}
</>
);
}
60 changes: 60 additions & 0 deletions src/pages/LetterDetail/components/LetterDetailHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useState } from 'react';

import { DeleteIcon, SirenOutlinedIcon } from '@/assets/icons';
import BackButton from '@/components/BackButton';
import useAuthStore from '@/stores/authStore';

import DegreeSelector from './DegreeSelector';
import LetterDetailDegreeButton from './LetterDetailDegreeButton';

interface LetterDetailHeader {
letterDetail: LetterDetail;
setLetterDetail: React.Dispatch<React.SetStateAction<LetterDetail>>;
setDeleteModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
setReportModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
export default function LetterDetailHeader({
letterDetail,
setLetterDetail,
setDeleteModalOpen,
setReportModalOpen,
}: LetterDetailHeader) {
const [degreeModalOpen, setDegreeModalOpen] = useState<boolean>(false);

const userZipCode = useAuthStore((state) => state.zipCode);

return (
<div className="absolute top-5 left-0 flex w-full justify-between px-5">
<BackButton />
<div className="flex gap-2">
{userZipCode !== letterDetail?.zipCode && (
<LetterDetailDegreeButton
letterDetail={letterDetail}
setDegreeModalOpen={setDegreeModalOpen}
/>
)}
{userZipCode === letterDetail?.zipCode && (
<button
onClick={() => {
setDeleteModalOpen(true);
}}
>
<DeleteIcon className="text-primary-1 h-6 w-6" />
</button>
)}
{userZipCode !== letterDetail?.zipCode && (
Copy link
Collaborator

Choose a reason for hiding this comment

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

이게 중복으로 등장하는 것 같아 상수로 빼면 깔끔할 것 같습니다!

<button
onClick={() => {
setReportModalOpen(true);
}}
>
<SirenOutlinedIcon className="text-primary-1 h-6 w-6" />
</button>
)}
{degreeModalOpen && (
<DegreeSelector letterDetail={letterDetail} setLetterDetail={setLetterDetail} />
)}
</div>
</div>
);
}
19 changes: 19 additions & 0 deletions src/pages/LetterDetail/components/LetterDetailReplyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useNavigate } from 'react-router';

interface LetterDetailReplyButton {
letterDetail: LetterDetail;
}
export default function LetterDetailReplyButton({ letterDetail }: LetterDetailReplyButton) {
const navigate = useNavigate();
return (
<button
className="bg-primary-3 disabled:bg-gray-30 body-m mt-3 w-full rounded-lg py-2 disabled:text-white"
onClick={() => {
navigate(`/letter/write/?letterId=${letterDetail.letterId}`);
}}
disabled={!letterDetail?.matched}
>
{letterDetail?.matched ? '편지 작성하기' : '대화가 종료된 편지입니다.'}
Copy link
Collaborator

Choose a reason for hiding this comment

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

aria-label 🙇🏻‍♀️

</button>
);
}
Loading