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
31 changes: 31 additions & 0 deletions src/api/record/createAiReview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { apiClient } from '../index';
import type { CreateAiReviewData, ApiResponse } from '@/types/record';

// API 응답 타입
export type CreateAiReviewResponse = ApiResponse<CreateAiReviewData>;

// AI 독서감상문 생성 API 함수
export const createAiReview = async (roomId: number) => {
const response = await apiClient.post<CreateAiReviewResponse>(
`/rooms/${roomId}/record/ai-review`,
);
return response.data;
};

/*
사용 예시:
try {
const result = await createAiReview(1);
if (result.isSuccess) {
console.log("생성된 독서감상문:", result.data.content);
console.log("잔여 이용 횟수:", result.data.count);
// 성공 처리 로직
} else {
console.error("AI 독서감상문 생성 실패:", result.message);
// 실패 처리 로직
}
} catch (error) {
console.error("API 호출 오류:", error);
// 에러 처리 로직
}
*/
31 changes: 31 additions & 0 deletions src/api/record/getAiUsage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { apiClient } from '../index';
import type { AiUsageData, ApiResponse } from '@/types/record';

// API 응답 타입
export type GetAiUsageResponse = ApiResponse<AiUsageData>;

// AI 이용 횟수 조회 API 함수
export const getAiUsage = async (roomId: number) => {
const response = await apiClient.get<GetAiUsageResponse>(
`/rooms/${roomId}/users/ai-usage`,
);
return response.data;
};

/*
사용 예시:
try {
const result = await getAiUsage(1);
if (result.isSuccess) {
console.log("AI 독서감상문 작성 가능 횟수:", result.data.recordReviewCount);
console.log("기록 작성 횟수:", result.data.recordCount);
// 성공 처리 로직
} else {
console.error("AI 이용 횟수 조회 실패:", result.message);
// 실패 처리 로직
}
} catch (error) {
console.error("API 호출 오류:", error);
// 에러 처리 로직
}
*/
4 changes: 3 additions & 1 deletion src/api/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export * from './createVote';
export * from './deleteRecord';
export * from './deleteVote';
export * from './postVote';
export * from './pinRecordToFeed';
export * from './pinRecordToFeed';
export * from './createAiReview';
export * from './getAiUsage';
76 changes: 62 additions & 14 deletions src/components/memory/MemoryAddButton/MemoryAddButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useRef, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { usePopupActions } from '@/hooks/usePopupActions';
import { getAiUsage } from '@/api/record';
import plusIcon from '../../../assets/memory/plus.svg';
import penIcon from '../../../assets/memory/pen.svg';
import voteIcon from '../../../assets/memory/vote.svg';
Expand All @@ -10,7 +11,7 @@ import { AddButton, DropdownContainer, DropdownItem } from './MemoryAddButton.st
const MemoryAddButton = () => {
const navigate = useNavigate();
const { roomId } = useParams<{ roomId: string }>(); // useParams 추가
const { openConfirm, closePopup } = usePopupActions();
const { openConfirm, closePopup, openSnackbar } = usePopupActions();
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);

Expand Down Expand Up @@ -55,20 +56,67 @@ const MemoryAddButton = () => {
console.log('투표 생성하기 - roomId:', currentRoomId);
};

const handleAIWrite = () => {
const handleAIWrite = async () => {
setIsOpen(false);
openConfirm({
title: 'AI 독서감상문 생성 (Beta)',
disc: '기록장에서 작성한 기록을 기반으로<br/>독서감상문을 생성하시겠어요?<br/>(서비스 내 잔여 이용횟수 : n/5)',
confirmText: '확인',
cancelText: '취소',
onConfirm: () => {
closePopup();
const currentRoomId = roomId || '1';
navigate(`/aiwrite/${currentRoomId}`);
console.log('AI 독서 감상문 생성 시작 - roomId:', currentRoomId);
},
});
const currentRoomId = roomId || '1';

try {
const result = await getAiUsage(Number(currentRoomId));

if (result.isSuccess) {
const { recordCount, recordReviewCount } = result.data;

// 기록이 2개 미만인 경우 에러 표시
if (recordCount < 2) {
openSnackbar({
message: `독후감 생성을 위해서는 최소 2개의 기록이 필요합니다. 현재 기록 개수: ${recordCount}`,
variant: 'top',
isError: true,
onClose: () => {},
});
return;
}

// 잔여 횟수가 5회 이상인 경우 (이미 5회 모두 사용)
if (recordReviewCount >= 5) {
openSnackbar({
message: '사용자의 독후감 작성 수가 5회를 초과했습니다.',
variant: 'top',
isError: true,
onClose: () => {},
});
return;
}

// 모달 표시
openConfirm({
title: 'AI 독서감상문 생성 (Beta)',
disc: `기록장에서 작성한 기록을 기반으로<br/>독서감상문을 생성하시겠어요?<br/>(서비스 내 잔여 이용횟수 : ${recordReviewCount}/5)`,
confirmText: '확인',
cancelText: '취소',
onConfirm: () => {
closePopup();
navigate(`/aiwrite/${currentRoomId}`);
console.log('AI 독서 감상문 생성 시작 - roomId:', currentRoomId);
},
});
} else {
openSnackbar({
message: result.message,
variant: 'top',
isError: true,
onClose: () => {},
});
}
} catch (error) {
console.error('AI 이용 횟수 조회 실패:', error);
openSnackbar({
message: 'AI 이용 횟수 조회에 실패했습니다',
variant: 'top',
isError: true,
onClose: () => {},
});
}
};

return (
Expand Down
67 changes: 58 additions & 9 deletions src/pages/aiwrite/AIWrite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,65 @@ import TitleHeader from '@/components/common/TitleHeader';
import { usePopupActions } from '@/hooks/usePopupActions';
import leftArrow from '@/assets/common/leftArrow.svg';
import infoIcon from '@/assets/common/infoIcon_white.svg';
import { MOCK_AI_WRITING } from '@/mocks/aiwrite.mock';
import { createAiReview } from '@/api/record';

const AIWrite = () => {
const navigate = useNavigate();
const { roomId } = useParams<{ roomId: string }>();
const { openSnackbar, openConfirm, closePopup } = usePopupActions();
const [isLoading, setIsLoading] = useState(true);
const [aiContent, setAiContent] = useState('');

useEffect(() => {
const timer = setTimeout(() => {
setIsLoading(false);
}, 5000);
const fetchAiReview = async () => {
if (!roomId) return;

try {
const result = await createAiReview(Number(roomId));

if (result.isSuccess) {
setAiContent(result.data.content);
} else {
openSnackbar({
message: result.message,
variant: 'top',
isError: true,
onClose: () => {},
});
navigate(`/rooms/${roomId}/memory`);
}
} catch (error) {
console.error('AI 독서감상문 생성 실패:', error);
let errorMessage = 'AI 독서감상문 생성에 실패했습니다';

if (error && typeof error === 'object' && 'response' in error) {
const axiosError = error as {
response?: {
data?: {
message?: string;
};
};
};
if (axiosError.response?.data?.message) {
errorMessage = axiosError.response.data.message;
}
}

openSnackbar({
message: errorMessage,
variant: 'top',
isError: true,
onClose: () => {},
});
navigate(`/rooms/${roomId}/memory`);
} finally {
setIsLoading(false);
}
};

return () => clearTimeout(timer);
}, []);
fetchAiReview();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [roomId]);

const handleBackClick = () => {
openConfirm({
Expand All @@ -38,7 +82,7 @@ const AIWrite = () => {

const handleCopyToClipboard = async () => {
try {
await navigator.clipboard.writeText(MOCK_AI_WRITING);
await navigator.clipboard.writeText(aiContent);
openSnackbar({
message: '클립보드에 복사가 완료되었어요',
variant: 'top',
Expand Down Expand Up @@ -77,7 +121,7 @@ const AIWrite = () => {
<img src={infoIcon} alt="정보" />
<span>내 기록과 총평을 바탕으로 생성된 감상문입니다.</span>
</InfoBanner>
<ContentText>{MOCK_AI_WRITING}</ContentText>
<ContentText>{aiContent}</ContentText>
<CopyButton onClick={handleCopyToClipboard}>클립보드에 복사</CopyButton>
</ResultContent>
)}
Expand All @@ -88,8 +132,10 @@ const AIWrite = () => {
const Container = styled.div`
display: flex;
flex-direction: column;
width: 100%;
min-width: 320px;
max-width: 767px;
min-height: 100vh;
margin: 0 auto;
background-color: ${colors.black.main};
padding-top: 56px;
`;
Expand Down Expand Up @@ -166,7 +212,10 @@ const ContentText = styled.div`
const CopyButton = styled.button`
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 767px;
height: 50px;
background-color: ${colors.purple.main};
color: ${colors.white};
Expand Down
12 changes: 12 additions & 0 deletions src/types/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ export interface UpdateVoteData {
roomId: number; // 방 ID
}

// AI 독서감상문 생성 응답 데이터 타입
export interface CreateAiReviewData {
content: string; // 생성된 독서감상문 내용
count: number; // 잔여 이용 횟수
}

// AI 이용 횟수 조회 응답 데이터 타입
export interface AiUsageData {
recordReviewCount: number; // AI 독서감상문 작성 가능 횟수
recordCount: number; // 기록 작성 횟수
}

// 공통 API 응답 타입
export interface ApiResponse<T> {
isSuccess: boolean;
Expand Down