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

// API 응답 타입
export type UpdateRecordResponse = ApiResponse<UpdateRecordData>;

// 기록 수정 API 함수
export const updateRecord = async (
roomId: number,
recordId: number,
recordData: UpdateRecordRequest,
): Promise<UpdateRecordResponse> => {
try {
const response = await apiClient.patch<UpdateRecordResponse>(
`/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);
// 에러 처리 로직
}
*/
44 changes: 44 additions & 0 deletions src/api/record/updateVote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { apiClient } from '../index';
import type { UpdateVoteRequest, UpdateVoteData, ApiResponse } from '@/types/record';

// API 응답 타입
export type UpdateVoteResponse = ApiResponse<UpdateVoteData>;

// 투표 수정 API 함수
export const updateVote = async (
roomId: number,
voteId: number,
voteData: UpdateVoteRequest,
): Promise<UpdateVoteResponse> => {
try {
const response = await apiClient.patch<UpdateVoteResponse>(
`/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);
// 에러 처리 로직
}
*/
21 changes: 21 additions & 0 deletions src/api/rooms/leaveRoom.ts
Original file line number Diff line number Diff line change
@@ -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<LeaveRoomResponse> => {
try {
const response = await apiClient.delete<LeaveRoomResponse>(`/rooms/${roomId}/leave`);

return response.data;
} catch (error) {
console.error('방 나가기 API 오류:', error);
throw error;
}
};
29 changes: 26 additions & 3 deletions src/components/memory/RecordItem/RecordItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,32 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
};

const handleEdit = useCallback(() => {
// API 개발 전까지 수정 기능 비활성화
console.log('수정 기능은 개발 중입니다.');
}, []);
if (!roomId) return;

// 바텀시트 닫기
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';
Expand Down
48 changes: 36 additions & 12 deletions src/components/pollwrite/PollCreationSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,23 @@ interface PollCreationSectionProps {
onContentChange: (value: string) => void;
options: string[];
onOptionsChange: (options: string[]) => void;
isEditMode?: boolean; // 수정 모드 여부
autoFocus?: boolean; // 자동 포커스 및 커서 위치 설정 여부
}

const PollCreationSection = ({
content,
onContentChange,
options,
onOptionsChange,
isEditMode = false,
autoFocus = false,
}: PollCreationSectionProps) => {
const maxContentLength = 20;
const maxOptions = 5;
const [focusStates, setFocusStates] = useState<boolean[]>(options.map(() => false));
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const contentInputRef = useRef<HTMLInputElement>(null);

const handleContentChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
Expand Down Expand Up @@ -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]);

Comment on lines +95 to +105

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

수정 모드에서 내용 로드 후 커서가 앞에 머무는 이슈. content 의존성 추가로 커서를 끝으로 이동.

초기 마운트 시점에 content가 아직 비어 있으면 커서가 앞에 위치할 수 있습니다. content를 의존성에 추가해 값이 로드된 뒤에도 커서를 끝으로 보내세요.

-  useEffect(() => {
-    if (autoFocus && contentInputRef.current) {
-      const input = contentInputRef.current;
-      input.focus();
-      // 커서를 텍스트 끝으로 이동
-      const length = input.value.length;
-      input.setSelectionRange(length, length);
-    }
-  }, [autoFocus]);
+  useEffect(() => {
+    if (!autoFocus || !contentInputRef.current) return;
+    const input = contentInputRef.current;
+    // 값 반영 후 커서 이동 보장을 위해 다음 tick에 실행
+    requestAnimationFrame(() => {
+      input.focus();
+      const length = input.value.length;
+      input.setSelectionRange(length, length);
+    });
+  }, [autoFocus, content]);
📝 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
// autoFocus 처리
useEffect(() => {
if (autoFocus && contentInputRef.current) {
const input = contentInputRef.current;
input.focus();
// 커서를 텍스트 끝으로 이동
const length = input.value.length;
input.setSelectionRange(length, length);
}
}, [autoFocus]);
// autoFocus 처리
useEffect(() => {
// autoFocus가 꺼져 있거나 ref가 없으면 아무 작업 없이 종료
if (!autoFocus || !contentInputRef.current) return;
const input = contentInputRef.current;
// 값 반영 후 커서 이동 보장을 위해 다음 tick에 실행
requestAnimationFrame(() => {
input.focus();
const length = input.value.length;
input.setSelectionRange(length, length);
});
}, [autoFocus, content]);
🤖 Prompt for AI Agents
In src/components/pollwrite/PollCreationSection.tsx around lines 95 to 105, the
useEffect that focuses the input only depends on autoFocus so when content is
loaded later (e.g. in edit mode) the cursor can remain at the start; add content
to the dependency array and ensure the effect runs when content changes so after
content is populated it focuses the input and moves the selection/cursor to the
end (keep the existing null checks for contentInputRef and autoFocus).

return (
<Section>
<PollContentContainer>
<PollInput
ref={contentInputRef}
placeholder="투표 내용을 20자 이내로 입력하세요."
value={content}
onChange={handleContentChange}
Expand All @@ -111,23 +128,30 @@ const PollCreationSection = ({
onFocus={() => handleOptionFocus(index)}
onBlur={() => handleOptionBlur(index)}
maxLength={20}
disabled={isEditMode}
readOnly={isEditMode}
/>
{/* 텍스트가 있을 때는 X 아이콘으로 텍스트 삭제 */}
{option.trim() !== '' && (
<DeleteButton onClick={() => handleClearOption(index)}>
<img src={closeIcon} alt="텍스트 삭제" />
</DeleteButton>
)}
{/* 텍스트가 없고 3번째 항목(index >= 2)부터만 쓰레기통 아이콘으로 항목 삭제 */}
{option.trim() === '' && index >= 2 && (
<DeleteButton onClick={() => handleRemoveOption(index)}>
<img src={trashIcon} alt="항목 삭제" />
</DeleteButton>
{/* 수정 모드가 아니어야만 삭제 버튼 표시 */}
{!isEditMode && (
<>
{/* 텍스트가 있을 때는 X 아이콘으로 텍스트 삭제 */}
{option.trim() !== '' && (
<DeleteButton onClick={() => handleClearOption(index)}>
<img src={closeIcon} alt="텍스트 삭제" />
</DeleteButton>
)}
{/* 텍스트가 없고 3번째 항목(index >= 2)부터만 쓰레기통 아이콘으로 항목 삭제 */}
{option.trim() === '' && index >= 2 && (
<DeleteButton onClick={() => handleRemoveOption(index)}>
<img src={trashIcon} alt="항목 삭제" />
</DeleteButton>
)}
</>
)}
</OptionInputContainer>
))}

{options.length < maxOptions && (
{!isEditMode && options.length < maxOptions && (
<AddOptionButton onClick={handleAddOption}>항목 추가</AddOptionButton>
)}
</PollOptionsContainer>
Expand Down
39 changes: 23 additions & 16 deletions src/components/recordwrite/PageRangeSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ interface PageRangeSectionProps {
onOverallToggle: () => void;
readingProgress: number;
isOverviewPossible: boolean;
isDisabled?: boolean;
hideToggle?: boolean; // 총평 토글 버튼 숨김 여부
}

const PageRangeSection = ({
Expand All @@ -45,6 +47,8 @@ const PageRangeSection = ({
isOverallEnabled,
onOverallToggle,
readingProgress,
isDisabled = false,
hideToggle = false,
}: PageRangeSectionProps) => {
const [hasError, setHasError] = useState(false);
const [showRedTooltip, setShowRedTooltip] = useState(false);
Expand Down Expand Up @@ -112,12 +116,13 @@ const PageRangeSection = ({
onChange={handleInputChange}
inputMode="numeric"
inputLength={pageRange.length || lastRecordedPage.toString().length}
disabled={isDisabled}
/>
<PageSuffix>/{totalPages}p</PageSuffix>
</InputWrapper>
)}
</PageInputContainer>
{!isOverallEnabled && pageRange && (
{!isOverallEnabled && pageRange && !isDisabled && (
<CloseButton onClick={handleClear}>
<img src={closeIcon} alt="지우기" />
</CloseButton>
Expand Down Expand Up @@ -148,21 +153,23 @@ const PageRangeSection = ({
<TooltipArrow variant="green" />
</Tooltip>
)}
<ToggleContainer>
<LeftSection>
<InfoIcon onClick={handleInfoClick}>
<img src={infoIcon} alt="정보" />
</InfoIcon>
<ToggleLabel disabled={!canUseOverall}>총평</ToggleLabel>
</LeftSection>
<ToggleSwitch
active={isOverallEnabled}
onClick={handleToggleClick}
disabled={!canUseOverall}
>
<ToggleSlider active={isOverallEnabled} disabled={!canUseOverall} />
</ToggleSwitch>
</ToggleContainer>
{!hideToggle && (
<ToggleContainer>
<LeftSection>
<InfoIcon onClick={handleInfoClick}>
<img src={infoIcon} alt="정보" />
</InfoIcon>
<ToggleLabel disabled={!canUseOverall}>총평</ToggleLabel>
</LeftSection>
<ToggleSwitch
active={isOverallEnabled}
onClick={handleToggleClick}
disabled={!canUseOverall || isDisabled}
>
<ToggleSlider active={isOverallEnabled} disabled={!canUseOverall} />
</ToggleSwitch>
</ToggleContainer>
)}
</TooltipContainer>
</Section>
);
Expand Down
20 changes: 17 additions & 3 deletions src/components/recordwrite/RecordContentSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLTextAreaElement>(null);

Expand All @@ -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<HTMLTextAreaElement>) => {
onContentChange(e.target.value);
Expand Down
Loading