From 26c6173a9e90d43058f6988b16cec6b07e12a857 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 9 Sep 2025 11:21:35 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EB=B0=A9=EB=82=98=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/rooms/leaveRoom.ts | 21 ++++++++ .../groupDetail/ParticipatedGroupDetail.tsx | 53 ++++++++++++++++--- 2 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 src/api/rooms/leaveRoom.ts diff --git a/src/api/rooms/leaveRoom.ts b/src/api/rooms/leaveRoom.ts new file mode 100644 index 00000000..11d2152f --- /dev/null +++ b/src/api/rooms/leaveRoom.ts @@ -0,0 +1,21 @@ +import { apiClient } from '../index'; + +// 방 나가기 응답 타입 +export interface LeaveRoomResponse { + isSuccess: boolean; + code: number; + message: string; + data: string; +} + +// 방 나가기 API 함수 +export const leaveRoom = async (roomId: number): Promise => { + try { + const response = await apiClient.delete(`/rooms/${roomId}/leave`); + + return response.data; + } catch (error) { + console.error('방 나가기 API 오류:', error); + throw error; + } +}; \ No newline at end of file diff --git a/src/pages/groupDetail/ParticipatedGroupDetail.tsx b/src/pages/groupDetail/ParticipatedGroupDetail.tsx index 38f35343..a1d12e05 100644 --- a/src/pages/groupDetail/ParticipatedGroupDetail.tsx +++ b/src/pages/groupDetail/ParticipatedGroupDetail.tsx @@ -34,6 +34,7 @@ import { convertVotesToPolls, type Poll, } from '@/api/rooms/getRoomPlaying'; +import { leaveRoom } from '@/api/rooms/leaveRoom'; import rightChevron from '../../assets/group/right-chevron.svg'; import leftArrow from '../../assets/common/leftArrow.svg'; import moreIcon from '../../assets/common/more.svg'; @@ -124,14 +125,50 @@ const ParticipatedGroupDetail = () => { openConfirm({ title: '모임방을 나가시겠어요?', disc: '방을 나가시게 되면\n독서메이트들과의 추억이 사라집니다.', - onConfirm: () => { - console.log('방 나가기 확정'); - openSnackbar({ - message: '나가기 기능은 현재 개발 중입니다.', - variant: 'top', - isError: true, - onClose: () => {}, - }); + onConfirm: async () => { + if (!roomId) return; + + try { + const response = await leaveRoom(parseInt(roomId)); + + if (response.isSuccess) { + openSnackbar({ + message: '모임 나가기를 완료했어요.', + variant: 'top', + isError: false, + onClose: () => {}, + }); + // 모임 홈으로 이동 + navigate('/group', { replace: true }); + } else { + // API에서 반환한 에러 메시지 사용 + openSnackbar({ + message: response.message, + variant: 'top', + isError: true, + onClose: () => {}, + }); + } + } catch (error: unknown) { + console.error('방 나가기 오류:', error); + + // 서버 응답이 있는 경우 메시지 사용, 없으면 기본 메시지 + let errorMessage = '방 나가기 중 오류가 발생했습니다.'; + + 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: () => {}, + }); + } }, }); }; From 1c27ed1d7236854a90b607ca77b38722462b4720 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 9 Sep 2025 11:47:17 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EA=B8=B0=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/record/updateRecord.ts | 44 ++++ .../memory/RecordItem/RecordItem.tsx | 17 +- .../recordwrite/PageRangeSection.tsx | 39 ++-- src/pages/index.tsx | 1 + src/pages/recordwrite/RecordWrite.tsx | 202 +++++++++++++----- src/types/record.ts | 10 + 6 files changed, 235 insertions(+), 78 deletions(-) create mode 100644 src/api/record/updateRecord.ts diff --git a/src/api/record/updateRecord.ts b/src/api/record/updateRecord.ts new file mode 100644 index 00000000..8c15b7c6 --- /dev/null +++ b/src/api/record/updateRecord.ts @@ -0,0 +1,44 @@ +import { apiClient } from '../index'; +import type { UpdateRecordRequest, UpdateRecordData, ApiResponse } from '@/types/record'; + +// API 응답 타입 +export type UpdateRecordResponse = ApiResponse; + +// 기록 수정 API 함수 +export const updateRecord = async ( + roomId: number, + recordId: number, + recordData: UpdateRecordRequest, +): Promise => { + try { + const response = await apiClient.patch( + `/rooms/${roomId}/records/${recordId}`, + recordData, + ); + return response.data; + } catch (error) { + console.error('기록 수정 API 오류:', error); + throw error; + } +}; + +/* +사용 예시: +const recordData: UpdateRecordRequest = { + content: "수정된 기록 내용입니다~~" +}; + +try { + const result = await updateRecord(1, 123, recordData); + if (result.isSuccess) { + console.log("수정된 방 ID:", result.data.roomId); + // 성공 처리 로직 + } else { + console.error("기록 수정 실패:", result.message); + // 실패 처리 로직 + } +} catch (error) { + console.error("API 호출 오류:", error); + // 에러 처리 로직 +} +*/ \ No newline at end of file diff --git a/src/components/memory/RecordItem/RecordItem.tsx b/src/components/memory/RecordItem/RecordItem.tsx index 5985b8cc..1dbe784a 100644 --- a/src/components/memory/RecordItem/RecordItem.tsx +++ b/src/components/memory/RecordItem/RecordItem.tsx @@ -125,9 +125,20 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => { }; const handleEdit = useCallback(() => { - // API 개발 전까지 수정 기능 비활성화 - console.log('수정 기능은 개발 중입니다.'); - }, []); + if (!roomId) return; + + // 바텀시트 닫기 + closePopup(); + + // 기록 수정 페이지로 이동 (기록 정보를 쿼리 파라미터로 전달) + const params = new URLSearchParams({ + content: content, + pageRange: pageRange || '', + recordType: recordType || 'normal' + }); + + navigate(`/memory/record/edit/${roomId}/${record.id}?${params.toString()}`); + }, [roomId, record.id, content, pageRange, recordType, navigate, closePopup]); const handleDelete = useCallback(async () => { const currentRoomId = roomId || '1'; diff --git a/src/components/recordwrite/PageRangeSection.tsx b/src/components/recordwrite/PageRangeSection.tsx index fd8d7692..3e69a515 100644 --- a/src/components/recordwrite/PageRangeSection.tsx +++ b/src/components/recordwrite/PageRangeSection.tsx @@ -35,6 +35,8 @@ interface PageRangeSectionProps { onOverallToggle: () => void; readingProgress: number; isOverviewPossible: boolean; + isDisabled?: boolean; + hideToggle?: boolean; // 총평 토글 버튼 숨김 여부 } const PageRangeSection = ({ @@ -45,6 +47,8 @@ const PageRangeSection = ({ isOverallEnabled, onOverallToggle, readingProgress, + isDisabled = false, + hideToggle = false, }: PageRangeSectionProps) => { const [hasError, setHasError] = useState(false); const [showRedTooltip, setShowRedTooltip] = useState(false); @@ -112,12 +116,13 @@ const PageRangeSection = ({ onChange={handleInputChange} inputMode="numeric" inputLength={pageRange.length || lastRecordedPage.toString().length} + disabled={isDisabled} /> /{totalPages}p )} - {!isOverallEnabled && pageRange && ( + {!isOverallEnabled && pageRange && !isDisabled && ( 지우기 @@ -148,21 +153,23 @@ const PageRangeSection = ({ )} - - - - 정보 - - 총평 - - - - - + {!hideToggle && ( + + + + 정보 + + 총평 + + + + + + )} ); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 4d886e1a..dc7f12ed 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -62,6 +62,7 @@ const Router = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/pages/recordwrite/RecordWrite.tsx b/src/pages/recordwrite/RecordWrite.tsx index a3f9ce98..49b97f3f 100644 --- a/src/pages/recordwrite/RecordWrite.tsx +++ b/src/pages/recordwrite/RecordWrite.tsx @@ -1,24 +1,30 @@ import { useState, useEffect } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import TitleHeader from '../../components/common/TitleHeader'; import PageRangeSection from '../../components/recordwrite/PageRangeSection'; import RecordContentSection from '../../components/recordwrite/RecordContentSection'; import leftArrow from '../../assets/common/leftArrow.svg'; import { Container } from './RecordWrite.styled'; import { createRecord } from '../../api/record/createRecord'; -import type { CreateRecordRequest } from '../../types/record'; +import { updateRecord } from '../../api/record/updateRecord'; +import type { CreateRecordRequest, UpdateRecordRequest } from '../../types/record'; import { getBookPage } from '../../api/rooms/getBookPage'; import { usePopupActions } from '../../hooks/usePopupActions'; const RecordWrite = () => { const navigate = useNavigate(); - const { roomId } = useParams<{ roomId: string }>(); + const { roomId, recordId } = useParams<{ roomId: string; recordId: string }>(); + const [searchParams] = useSearchParams(); + + // 수정 모드인지 판단 + const isEditMode = Boolean(recordId); const { openSnackbar } = usePopupActions(); const [pageRange, setPageRange] = useState(''); const [content, setContent] = useState(''); const [isOverallEnabled, setIsOverallEnabled] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); + // API에서 받아올 데이터 const [totalPages, setTotalPages] = useState(0); @@ -26,9 +32,9 @@ const RecordWrite = () => { const [isOverviewPossible, setIsOverviewPossible] = useState(false); const [isLoading, setIsLoading] = useState(true); - // 컴포넌트 마운트 시 책 페이지 정보 조회 + // 컴포넌트 마운트 시 책 페이지 정보 조회 (생성 모드) 또는 기록 내용 로드 (수정 모드) useEffect(() => { - const fetchBookPageInfo = async () => { + const initializeData = async () => { if (!roomId) { openSnackbar({ message: '방 정보를 찾을 수 없습니다.', @@ -41,6 +47,36 @@ const RecordWrite = () => { try { setIsLoading(true); + + if (isEditMode) { + // 수정 모드: 쿼리 파라미터에서 기존 내용과 페이지 정보 로드 + const existingContent = searchParams.get('content'); + const existingPageRange = searchParams.get('pageRange'); + const existingRecordType = searchParams.get('recordType'); + + if (existingContent) { + setContent(decodeURIComponent(existingContent)); + } + + if (existingPageRange) { + setPageRange(existingPageRange); + } + + if (existingRecordType === 'overall') { + setIsOverallEnabled(true); + } + + // 수정 모드에서도 전체 페이지 수는 필요하므로 책 정보 조회 + const response = await getBookPage(parseInt(roomId)); + if (response.isSuccess) { + setTotalPages(response.data.totalBookPage); + } + + setIsLoading(false); + return; + } + + // 생성 모드: 책 페이지 정보 조회 const response = await getBookPage(parseInt(roomId)); if (response.isSuccess) { @@ -55,9 +91,9 @@ const RecordWrite = () => { }); } } catch (error) { - console.error('책 페이지 정보 조회 오류:', error); + console.error('데이터 로드 오류:', error); - let errorMessage = '책 정보를 불러오는 중 오류가 발생했습니다.'; + let errorMessage = '데이터를 불러오는 중 오류가 발생했습니다.'; if (error && typeof error === 'object' && 'response' in error) { const axiosError = error as { @@ -90,8 +126,8 @@ const RecordWrite = () => { } }; - fetchBookPageInfo(); - }, [roomId]); + initializeData(); + }, [roomId, isEditMode]); // 총평 모드가 변경될 때 isOverviewPossible 체크 useEffect(() => { @@ -115,67 +151,113 @@ const RecordWrite = () => { setIsSubmitting(true); try { - // 페이지 범위 결정 - let finalPage: number; + if (isEditMode) { + // 수정 모드: 내용만 수정 + if (!recordId) { + openSnackbar({ + message: '기록 정보를 찾을 수 없습니다.', + variant: 'top', + onClose: () => {}, + }); + setIsSubmitting(false); + return; + } + + const updateData: UpdateRecordRequest = { + content: content.trim(), + }; + + console.log('기록 수정 API 호출:', updateData); + console.log('roomId:', roomId, 'recordId:', recordId); + + const response = await updateRecord(parseInt(roomId), parseInt(recordId), updateData); - if (isOverallEnabled) { - // 총평인 경우: 책의 마지막 페이지 또는 전체 페이지 수 사용 - finalPage = totalPages; + if (response.isSuccess) { + console.log('기록 수정 성공:', response.data); + + openSnackbar({ + message: '기록 수정을 완료했어요.', + variant: 'top', + onClose: () => {}, + }); + + // 성공 시 기록장으로 이동 + navigate(`/rooms/${roomId}/memory`, { + replace: true, + }); + } else { + console.error('기록 수정 실패:', response.message); + openSnackbar({ + message: response.message || '기록 수정에 실패했습니다.', + variant: 'top', + onClose: () => {}, + }); + setIsSubmitting(false); + } } else { - // 일반 기록인 경우 - if (pageRange.trim() !== '') { - finalPage = parseInt(pageRange.trim()); + // 생성 모드: 기존 로직 유지 + // 페이지 범위 결정 + let finalPage: number; + + if (isOverallEnabled) { + // 총평인 경우: 책의 마지막 페이지 또는 전체 페이지 수 사용 + finalPage = totalPages; } else { - finalPage = lastRecordedPage; + // 일반 기록인 경우 + if (pageRange.trim() !== '') { + finalPage = parseInt(pageRange.trim()); + } else { + finalPage = lastRecordedPage; + } } - } - // 페이지 유효성 검사 - if (finalPage <= 0 || finalPage > totalPages) { - openSnackbar({ - message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`, - variant: 'top', - onClose: () => {}, - }); - setIsSubmitting(false); - return; - } + // 페이지 유효성 검사 + if (finalPage <= 0 || finalPage > totalPages) { + openSnackbar({ + message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`, + variant: 'top', + onClose: () => {}, + }); + setIsSubmitting(false); + return; + } - // API 요청 데이터 생성 - const recordData: CreateRecordRequest = { - page: finalPage, - isOverview: isOverallEnabled, - content: content.trim(), - }; + // API 요청 데이터 생성 + const recordData: CreateRecordRequest = { + page: finalPage, + isOverview: isOverallEnabled, + content: content.trim(), + }; - console.log('기록 생성 API 호출:', recordData); - console.log('roomId:', roomId); + console.log('기록 생성 API 호출:', recordData); + console.log('roomId:', roomId); - // API 호출 - const response = await createRecord(parseInt(roomId), recordData); + // API 호출 + const response = await createRecord(parseInt(roomId), recordData); - if (response.isSuccess) { - console.log('기록 생성 성공:', response.data); + if (response.isSuccess) { + console.log('기록 생성 성공:', response.data); - // 성공 시 기록장으로 이동 - navigate(`/rooms/${roomId}/memory`, { - replace: true, - }); - } else { - // API 에러 응답 처리 - console.error('기록 생성 실패:', response.message); - openSnackbar({ - message: response.message || '기록 작성에 실패했습니다.', - variant: 'top', - onClose: () => {}, - }); - setIsSubmitting(false); + // 성공 시 기록장으로 이동 + navigate(`/rooms/${roomId}/memory`, { + replace: true, + }); + } else { + // API 에러 응답 처리 + console.error('기록 생성 실패:', response.message); + openSnackbar({ + message: response.message || '기록 작성에 실패했습니다.', + variant: 'top', + onClose: () => {}, + }); + setIsSubmitting(false); + } } } catch (error) { console.error('기록 저장 실패:', error); // 에러 타입에 따른 메시지 처리 - let errorMessage = '기록 저장 중 오류가 발생했습니다.'; + let errorMessage = isEditMode ? '기록 수정 중 오류가 발생했습니다.' : '기록 저장 중 오류가 발생했습니다.'; if (error && typeof error === 'object' && 'response' in error) { const axiosError = error as { @@ -192,9 +274,9 @@ const RecordWrite = () => { } else if (axiosError.response?.data?.code === 400) { errorMessage = '입력값을 확인해 주세요.'; } else if (axiosError.response?.data?.code === 403) { - errorMessage = '방 접근 권한이 없습니다.'; + errorMessage = '접근 권한이 없습니다.'; } else if (axiosError.response?.data?.code === 404) { - errorMessage = '존재하지 않는 방입니다.'; + errorMessage = '존재하지 않는 데이터입니다.'; } } @@ -213,7 +295,7 @@ const RecordWrite = () => { <> } - title="기록 작성" + title={isEditMode ? "기록 수정" : "기록 작성"} onLeftClick={handleBackClick} /> @@ -237,7 +319,7 @@ const RecordWrite = () => { <> } - title="기록 작성" + title={isEditMode ? "기록 수정" : "기록 작성"} rightButton={
완료
} onLeftClick={handleBackClick} onRightClick={handleCompleteClick} @@ -253,6 +335,8 @@ const RecordWrite = () => { onOverallToggle={() => setIsOverallEnabled(prev => !prev)} readingProgress={isOverviewPossible ? 80 : 70} // 총평 가능하면 80% 이상으로 표시 isOverviewPossible={isOverviewPossible} + isDisabled={isEditMode} // 수정 모드일 때 비활성화 + hideToggle={isEditMode} // 수정 모드일 때 총평 토글 숨김 />
diff --git a/src/types/record.ts b/src/types/record.ts index 6b832a5f..8ce5e468 100644 --- a/src/types/record.ts +++ b/src/types/record.ts @@ -51,6 +51,16 @@ export interface VoteData { voteItems: VoteItemResult[]; // 투표 결과 리스트 } +// 기록 수정 요청 데이터 타입 +export interface UpdateRecordRequest { + content: string; // 수정할 기록 내용 +} + +// 기록 수정 응답 데이터 타입 +export interface UpdateRecordData { + roomId: number; // 방 ID +} + // 공통 API 응답 타입 export interface ApiResponse { isSuccess: boolean; From d05d8ca0506fa661e60ea47a882156e849361c0a Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 9 Sep 2025 14:12:41 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=ED=88=AC=ED=91=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20API=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EC=BB=A4=EC=84=9C=20=EB=A7=88=EC=A7=80?= =?UTF-8?q?=EB=A7=89=EC=97=90=20=EC=9C=84=EC=B9=98=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/record/updateVote.ts | 44 +++++ .../memory/RecordItem/RecordItem.tsx | 30 ++- .../pollwrite/PollCreationSection.tsx | 48 +++-- .../recordwrite/RecordContentSection.tsx | 20 +- src/pages/index.tsx | 1 + src/pages/pollwrite/PollWrite.tsx | 185 +++++++++++++----- src/pages/recordwrite/RecordWrite.tsx | 6 +- src/types/record.ts | 10 + 8 files changed, 274 insertions(+), 70 deletions(-) create mode 100644 src/api/record/updateVote.ts diff --git a/src/api/record/updateVote.ts b/src/api/record/updateVote.ts new file mode 100644 index 00000000..d9589eec --- /dev/null +++ b/src/api/record/updateVote.ts @@ -0,0 +1,44 @@ +import { apiClient } from '../index'; +import type { UpdateVoteRequest, UpdateVoteData, ApiResponse } from '@/types/record'; + +// API 응답 타입 +export type UpdateVoteResponse = ApiResponse; + +// 투표 수정 API 함수 +export const updateVote = async ( + roomId: number, + voteId: number, + voteData: UpdateVoteRequest, +): Promise => { + try { + const response = await apiClient.patch( + `/rooms/${roomId}/votes/${voteId}`, + voteData, + ); + return response.data; + } catch (error) { + console.error('투표 수정 API 오류:', error); + throw error; + } +}; + +/* +사용 예시: +const voteData: UpdateVoteRequest = { + content: "수정된 투표 내용입니다~" +}; + +try { + const result = await updateVote(1, 123, voteData); + if (result.isSuccess) { + console.log("수정된 방 ID:", result.data.roomId); + // 성공 처리 로직 + } else { + console.error("투표 수정 실패:", result.message); + // 실패 처리 로직 + } +} catch (error) { + console.error("API 호출 오류:", error); + // 에러 처리 로직 +} +*/ \ No newline at end of file diff --git a/src/components/memory/RecordItem/RecordItem.tsx b/src/components/memory/RecordItem/RecordItem.tsx index 1dbe784a..ea71c59f 100644 --- a/src/components/memory/RecordItem/RecordItem.tsx +++ b/src/components/memory/RecordItem/RecordItem.tsx @@ -130,15 +130,27 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => { // 바텀시트 닫기 closePopup(); - // 기록 수정 페이지로 이동 (기록 정보를 쿼리 파라미터로 전달) - const params = new URLSearchParams({ - content: content, - pageRange: pageRange || '', - recordType: recordType || 'normal' - }); - - navigate(`/memory/record/edit/${roomId}/${record.id}?${params.toString()}`); - }, [roomId, record.id, content, pageRange, recordType, navigate, closePopup]); + if (type === 'poll') { + // 투표 수정 페이지로 이동 (투표 정보를 쿼리 파라미터로 전달) + const params = new URLSearchParams({ + content: content, + pageRange: pageRange || '', + recordType: recordType || 'normal', + options: JSON.stringify(pollOptions?.map(option => option.text) || []) + }); + + navigate(`/memory/poll/edit/${roomId}/${record.id}?${params.toString()}`); + } else { + // 기록 수정 페이지로 이동 (기록 정보를 쿼리 파라미터로 전달) + const params = new URLSearchParams({ + content: content, + pageRange: pageRange || '', + recordType: recordType || 'normal' + }); + + navigate(`/memory/record/edit/${roomId}/${record.id}?${params.toString()}`); + } + }, [roomId, record.id, content, pageRange, recordType, type, pollOptions, navigate, closePopup]); const handleDelete = useCallback(async () => { const currentRoomId = roomId || '1'; diff --git a/src/components/pollwrite/PollCreationSection.tsx b/src/components/pollwrite/PollCreationSection.tsx index 5bc0ce8f..92b5dca1 100644 --- a/src/components/pollwrite/PollCreationSection.tsx +++ b/src/components/pollwrite/PollCreationSection.tsx @@ -17,6 +17,8 @@ interface PollCreationSectionProps { onContentChange: (value: string) => void; options: string[]; onOptionsChange: (options: string[]) => void; + isEditMode?: boolean; // 수정 모드 여부 + autoFocus?: boolean; // 자동 포커스 및 커서 위치 설정 여부 } const PollCreationSection = ({ @@ -24,11 +26,14 @@ const PollCreationSection = ({ onContentChange, options, onOptionsChange, + isEditMode = false, + autoFocus = false, }: PollCreationSectionProps) => { const maxContentLength = 20; const maxOptions = 5; const [focusStates, setFocusStates] = useState(options.map(() => false)); const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + const contentInputRef = useRef(null); const handleContentChange = (e: React.ChangeEvent) => { const value = e.target.value; @@ -87,10 +92,22 @@ const PollCreationSection = ({ } }, [options.length, focusStates.length]); + // autoFocus 처리 + useEffect(() => { + if (autoFocus && contentInputRef.current) { + const input = contentInputRef.current; + input.focus(); + // 커서를 텍스트 끝으로 이동 + const length = input.value.length; + input.setSelectionRange(length, length); + } + }, [autoFocus]); + return (
handleOptionFocus(index)} onBlur={() => handleOptionBlur(index)} maxLength={20} + disabled={isEditMode} + readOnly={isEditMode} /> - {/* 텍스트가 있을 때는 X 아이콘으로 텍스트 삭제 */} - {option.trim() !== '' && ( - handleClearOption(index)}> - 텍스트 삭제 - - )} - {/* 텍스트가 없고 3번째 항목(index >= 2)부터만 쓰레기통 아이콘으로 항목 삭제 */} - {option.trim() === '' && index >= 2 && ( - handleRemoveOption(index)}> - 항목 삭제 - + {/* 수정 모드가 아니어야만 삭제 버튼 표시 */} + {!isEditMode && ( + <> + {/* 텍스트가 있을 때는 X 아이콘으로 텍스트 삭제 */} + {option.trim() !== '' && ( + handleClearOption(index)}> + 텍스트 삭제 + + )} + {/* 텍스트가 없고 3번째 항목(index >= 2)부터만 쓰레기통 아이콘으로 항목 삭제 */} + {option.trim() === '' && index >= 2 && ( + handleRemoveOption(index)}> + 항목 삭제 + + )} + )} ))} - {options.length < maxOptions && ( + {!isEditMode && options.length < maxOptions && ( 항목 추가 )} diff --git a/src/components/recordwrite/RecordContentSection.tsx b/src/components/recordwrite/RecordContentSection.tsx index 5bd53389..15b55411 100644 --- a/src/components/recordwrite/RecordContentSection.tsx +++ b/src/components/recordwrite/RecordContentSection.tsx @@ -10,9 +10,14 @@ import { interface RecordContentSectionProps { content: string; onContentChange: (value: string) => void; + autoFocus?: boolean; // 자동 포커스 및 커서 위치 설정 여부 } -const RecordContentSection = ({ content, onContentChange }: RecordContentSectionProps) => { +const RecordContentSection = ({ + content, + onContentChange, + autoFocus = false, +}: RecordContentSectionProps) => { const maxLength = 500; const textareaRef = useRef(null); @@ -31,10 +36,19 @@ const RecordContentSection = ({ content, onContentChange }: RecordContentSection adjustHeight(); }, [content]); - // 컴포넌트 마운트 시 초기 높이 설정 + // 컴포넌트 마운트 시 초기 높이 설정 및 autoFocus 처리 useEffect(() => { adjustHeight(); - }, []); + + // autoFocus가 true이고 textarea가 있으면 포커스 및 커서를 끝으로 이동 + if (autoFocus && textareaRef.current) { + const textarea = textareaRef.current; + textarea.focus(); + // 커서를 텍스트 끝으로 이동 + const length = textarea.value.length; + textarea.setSelectionRange(length, length); + } + }, [autoFocus]); const handleChange = (e: React.ChangeEvent) => { onContentChange(e.target.value); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index dc7f12ed..fe34062b 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -64,6 +64,7 @@ const Router = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/pages/pollwrite/PollWrite.tsx b/src/pages/pollwrite/PollWrite.tsx index 197676c1..914b273c 100644 --- a/src/pages/pollwrite/PollWrite.tsx +++ b/src/pages/pollwrite/PollWrite.tsx @@ -1,18 +1,23 @@ import { useState, useEffect } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import TitleHeader from '../../components/common/TitleHeader'; import PageRangeSection from '../../components/recordwrite/PageRangeSection'; import PollCreationSection from '../../components/pollwrite/PollCreationSection'; import leftArrow from '../../assets/common/leftArrow.svg'; import { Container } from './PollWrite.styled'; import { createVote } from '../../api/record/createVote'; -import type { CreateVoteRequest } from '../../types/record'; +import { updateVote } from '../../api/record/updateVote'; +import type { CreateVoteRequest, UpdateVoteRequest } from '../../types/record'; import { getBookPage } from '../../api/rooms/getBookPage'; import { usePopupActions } from '../../hooks/usePopupActions'; const PollWrite = () => { const navigate = useNavigate(); - const { roomId } = useParams<{ roomId: string }>(); + const { roomId, voteId } = useParams<{ roomId: string; voteId: string }>(); + const [searchParams] = useSearchParams(); + + // 수정 모드인지 판단 + const isEditMode = Boolean(voteId); const { openSnackbar } = usePopupActions(); const [pageRange, setPageRange] = useState(''); @@ -27,9 +32,9 @@ const PollWrite = () => { const [isOverviewPossible, setIsOverviewPossible] = useState(false); const [isLoading, setIsLoading] = useState(true); - // 컴포넌트 마운트 시 책 페이지 정보 조회 + // 컴포넌트 마운트 시 책 페이지 정보 조회 (생성 모드) 또는 투표 내용 로드 (수정 모드) useEffect(() => { - const fetchBookPageInfo = async () => { + const initializeData = async () => { if (!roomId) { openSnackbar({ message: '방 정보를 찾을 수 없습니다.', @@ -42,6 +47,46 @@ const PollWrite = () => { try { setIsLoading(true); + + if (isEditMode) { + // 수정 모드: 쿼리 파라미터에서 기존 내용과 페이지 정보, 투표 옵션 로드 + const existingContent = searchParams.get('content'); + const existingPageRange = searchParams.get('pageRange'); + const existingRecordType = searchParams.get('recordType'); + const existingOptions = searchParams.get('options'); + + if (existingContent) { + setPollContent(decodeURIComponent(existingContent)); + } + + if (existingPageRange) { + setPageRange(existingPageRange); + } + + if (existingRecordType === 'overall') { + setIsOverallEnabled(true); + } + + if (existingOptions) { + try { + const options = JSON.parse(decodeURIComponent(existingOptions)); + setPollOptions(options); + } catch (e) { + console.error('투표 옵션 파싱 오류:', e); + } + } + + // 수정 모드에서도 전체 페이지 수는 필요하므로 책 정보 조회 + const response = await getBookPage(parseInt(roomId)); + if (response.isSuccess) { + setTotalPages(response.data.totalBookPage); + } + + setIsLoading(false); + return; + } + + // 생성 모드: 책 페이지 정보 조회 const response = await getBookPage(parseInt(roomId)); if (response.isSuccess) { @@ -56,9 +101,9 @@ const PollWrite = () => { }); } } catch (error) { - console.error('책 페이지 정보 조회 오류:', error); + console.error('데이터 로드 오류:', error); - let errorMessage = '책 정보를 불러오는 중 오류가 발생했습니다.'; + let errorMessage = '데이터를 불러오는 중 오류가 발생했습니다.'; if (error && typeof error === 'object' && 'response' in error) { const axiosError = error as { @@ -91,8 +136,8 @@ const PollWrite = () => { } }; - fetchBookPageInfo(); - }, [roomId]); + initializeData(); + }, [roomId, isEditMode]); // 총평 모드가 변경될 때 isOverviewPossible 체크 useEffect(() => { @@ -116,47 +161,92 @@ const PollWrite = () => { setIsSubmitting(true); try { - // 투표 옵션 필터링 (빈 옵션 제거) - const validOptions = pollOptions.filter(option => option.trim() !== ''); + if (isEditMode) { + // 수정 모드: 내용만 수정 + if (!voteId) { + openSnackbar({ + message: '투표 정보를 찾을 수 없습니다.', + variant: 'top', + onClose: () => {}, + }); + setIsSubmitting(false); + return; + } - if (validOptions.length < 2) { - openSnackbar({ - message: '투표 옵션은 최소 2개 이상이어야 합니다.', - variant: 'top', - onClose: () => {}, - }); - setIsSubmitting(false); - return; - } + const updateData: UpdateVoteRequest = { + content: pollContent.trim(), + }; + + console.log('투표 수정 API 호출:', updateData); + console.log('roomId:', roomId, 'voteId:', voteId); + + const response = await updateVote(parseInt(roomId), parseInt(voteId), updateData); - // 페이지 범위 결정 - let finalPage: number; + if (response.isSuccess) { + console.log('투표 수정 성공:', response.data); + + openSnackbar({ + message: '투표 수정을 완료했어요.', + variant: 'top', + onClose: () => {}, + }); - if (isOverallEnabled) { - // 총평인 경우: 책의 마지막 페이지 또는 전체 페이지 수 사용 - finalPage = totalPages; + // 성공 시 기록장으로 이동 + navigate(`/rooms/${roomId}/memory`, { + replace: true, + }); + } else { + console.error('투표 수정 실패:', response.message); + openSnackbar({ + message: response.message || '투표 수정에 실패했습니다.', + variant: 'top', + onClose: () => {}, + }); + setIsSubmitting(false); + } } else { - // 일반 투표인 경우 - if (pageRange.trim() !== '') { - finalPage = parseInt(pageRange.trim()); + // 생성 모드: 기존 로직 유지 + // 투표 옵션 필터링 (빈 옵션 제거) + const validOptions = pollOptions.filter(option => option.trim() !== ''); + + if (validOptions.length < 2) { + openSnackbar({ + message: '투표 옵션은 최소 2개 이상이어야 합니다.', + variant: 'top', + onClose: () => {}, + }); + setIsSubmitting(false); + return; + } + + // 페이지 범위 결정 + let finalPage: number; + + if (isOverallEnabled) { + // 총평인 경우: 책의 마지막 페이지 또는 전체 페이지 수 사용 + finalPage = totalPages; } else { - finalPage = lastRecordedPage; + // 일반 투표인 경우 + if (pageRange.trim() !== '') { + finalPage = parseInt(pageRange.trim()); + } else { + finalPage = lastRecordedPage; + } } - } - // 페이지 유효성 검사 - if (finalPage <= 0 || finalPage > totalPages) { - openSnackbar({ - message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`, - variant: 'top', - onClose: () => {}, - }); - setIsSubmitting(false); - return; - } + // 페이지 유효성 검사 + if (finalPage <= 0 || finalPage > totalPages) { + openSnackbar({ + message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`, + variant: 'top', + onClose: () => {}, + }); + setIsSubmitting(false); + return; + } - // API 요청 데이터 생성 - const voteData: CreateVoteRequest = { + // API 요청 데이터 생성 + const voteData: CreateVoteRequest = { page: finalPage, isOverview: isOverallEnabled, content: pollContent.trim(), @@ -186,11 +276,12 @@ const PollWrite = () => { }); setIsSubmitting(false); } + } } catch (error) { console.error('투표 저장 실패:', error); // 에러 타입에 따른 메시지 처리 - let errorMessage = '투표 저장 중 오류가 발생했습니다.'; + let errorMessage = isEditMode ? '투표 수정 중 오류가 발생했습니다.' : '투표 저장 중 오류가 발생했습니다.'; if (error && typeof error === 'object' && 'response' in error) { const axiosError = error as { @@ -228,7 +319,7 @@ const PollWrite = () => { <> } - title="투표 작성" + title={isEditMode ? "투표 수정" : "투표 작성"} onLeftClick={handleBackClick} /> @@ -252,7 +343,7 @@ const PollWrite = () => { <> } - title="투표 작성" + title={isEditMode ? "투표 수정" : "투표 작성"} rightButton={
완료
} onLeftClick={handleBackClick} onRightClick={handleCompleteClick} @@ -268,12 +359,16 @@ const PollWrite = () => { onOverallToggle={() => setIsOverallEnabled(prev => !prev)} readingProgress={isOverviewPossible ? 80 : 70} // 총평 가능하면 80% 이상으로 표시 isOverviewPossible={isOverviewPossible} + isDisabled={isEditMode} // 수정 모드일 때 비활성화 + hideToggle={isEditMode} // 수정 모드일 때 총평 토글 숨김 />
diff --git a/src/pages/recordwrite/RecordWrite.tsx b/src/pages/recordwrite/RecordWrite.tsx index 49b97f3f..9622135f 100644 --- a/src/pages/recordwrite/RecordWrite.tsx +++ b/src/pages/recordwrite/RecordWrite.tsx @@ -338,7 +338,11 @@ const RecordWrite = () => { isDisabled={isEditMode} // 수정 모드일 때 비활성화 hideToggle={isEditMode} // 수정 모드일 때 총평 토글 숨김 /> - + ); diff --git a/src/types/record.ts b/src/types/record.ts index 8ce5e468..878e68a9 100644 --- a/src/types/record.ts +++ b/src/types/record.ts @@ -61,6 +61,16 @@ export interface UpdateRecordData { roomId: number; // 방 ID } +// 투표 수정 요청 데이터 타입 +export interface UpdateVoteRequest { + content: string; // 수정할 투표 내용 +} + +// 투표 수정 응답 데이터 타입 +export interface UpdateVoteData { + roomId: number; // 방 ID +} + // 공통 API 응답 타입 export interface ApiResponse { isSuccess: boolean; From 5ba442486933a7b6f09bd50288795636a532ae1e Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 9 Sep 2025 15:11:21 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20console.log=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/recordwrite/RecordWrite.tsx | 48 +++++++++------------------ 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/src/pages/recordwrite/RecordWrite.tsx b/src/pages/recordwrite/RecordWrite.tsx index 9622135f..f233fb0a 100644 --- a/src/pages/recordwrite/RecordWrite.tsx +++ b/src/pages/recordwrite/RecordWrite.tsx @@ -15,7 +15,7 @@ const RecordWrite = () => { const navigate = useNavigate(); const { roomId, recordId } = useParams<{ roomId: string; recordId: string }>(); const [searchParams] = useSearchParams(); - + // 수정 모드인지 판단 const isEditMode = Boolean(recordId); const { openSnackbar } = usePopupActions(); @@ -24,7 +24,6 @@ const RecordWrite = () => { const [content, setContent] = useState(''); const [isOverallEnabled, setIsOverallEnabled] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); - // API에서 받아올 데이터 const [totalPages, setTotalPages] = useState(0); @@ -47,35 +46,35 @@ const RecordWrite = () => { try { setIsLoading(true); - + if (isEditMode) { // 수정 모드: 쿼리 파라미터에서 기존 내용과 페이지 정보 로드 const existingContent = searchParams.get('content'); const existingPageRange = searchParams.get('pageRange'); const existingRecordType = searchParams.get('recordType'); - + if (existingContent) { setContent(decodeURIComponent(existingContent)); } - + if (existingPageRange) { setPageRange(existingPageRange); } - + if (existingRecordType === 'overall') { setIsOverallEnabled(true); } - + // 수정 모드에서도 전체 페이지 수는 필요하므로 책 정보 조회 const response = await getBookPage(parseInt(roomId)); if (response.isSuccess) { setTotalPages(response.data.totalBookPage); } - + setIsLoading(false); return; } - + // 생성 모드: 책 페이지 정보 조회 const response = await getBookPage(parseInt(roomId)); @@ -91,8 +90,6 @@ const RecordWrite = () => { }); } } catch (error) { - console.error('데이터 로드 오류:', error); - let errorMessage = '데이터를 불러오는 중 오류가 발생했습니다.'; if (error && typeof error === 'object' && 'response' in error) { @@ -167,14 +164,9 @@ const RecordWrite = () => { content: content.trim(), }; - console.log('기록 수정 API 호출:', updateData); - console.log('roomId:', roomId, 'recordId:', recordId); - const response = await updateRecord(parseInt(roomId), parseInt(recordId), updateData); if (response.isSuccess) { - console.log('기록 수정 성공:', response.data); - openSnackbar({ message: '기록 수정을 완료했어요.', variant: 'top', @@ -186,7 +178,6 @@ const RecordWrite = () => { replace: true, }); } else { - console.error('기록 수정 실패:', response.message); openSnackbar({ message: response.message || '기록 수정에 실패했습니다.', variant: 'top', @@ -229,22 +220,15 @@ const RecordWrite = () => { content: content.trim(), }; - console.log('기록 생성 API 호출:', recordData); - console.log('roomId:', roomId); - // API 호출 const response = await createRecord(parseInt(roomId), recordData); if (response.isSuccess) { - console.log('기록 생성 성공:', response.data); - // 성공 시 기록장으로 이동 navigate(`/rooms/${roomId}/memory`, { replace: true, }); } else { - // API 에러 응답 처리 - console.error('기록 생성 실패:', response.message); openSnackbar({ message: response.message || '기록 작성에 실패했습니다.', variant: 'top', @@ -254,10 +238,10 @@ const RecordWrite = () => { } } } catch (error) { - console.error('기록 저장 실패:', error); - // 에러 타입에 따른 메시지 처리 - let errorMessage = isEditMode ? '기록 수정 중 오류가 발생했습니다.' : '기록 저장 중 오류가 발생했습니다.'; + let errorMessage = isEditMode + ? '기록 수정 중 오류가 발생했습니다.' + : '기록 저장 중 오류가 발생했습니다.'; if (error && typeof error === 'object' && 'response' in error) { const axiosError = error as { @@ -295,7 +279,7 @@ const RecordWrite = () => { <> } - title={isEditMode ? "기록 수정" : "기록 작성"} + title={isEditMode ? '기록 수정' : '기록 작성'} onLeftClick={handleBackClick} /> @@ -319,7 +303,7 @@ const RecordWrite = () => { <> } - title={isEditMode ? "기록 수정" : "기록 작성"} + title={isEditMode ? '기록 수정' : '기록 작성'} rightButton={
완료
} onLeftClick={handleBackClick} onRightClick={handleCompleteClick} @@ -338,9 +322,9 @@ const RecordWrite = () => { isDisabled={isEditMode} // 수정 모드일 때 비활성화 hideToggle={isEditMode} // 수정 모드일 때 총평 토글 숨김 /> -