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
58 changes: 58 additions & 0 deletions src/api/rooms/createDailyGreeting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { apiClient } from '../index';

// 오늘의 한마디 작성 요청 데이터 타입
export interface CreateDailyGreetingRequest {
content: string; // 오늘의 한마디 작성 내용
}

// 오늘의 한마디 작성 응답 데이터 타입
export interface CreateDailyGreetingData {
attendanceCheckId: number; // 출석체크 ID
}

// API 응답 타입
export interface CreateDailyGreetingResponse {
isSuccess: boolean;
code: number;
message: string;
data: CreateDailyGreetingData;
}

// 오늘의 한마디 작성 API 함수
export const createDailyGreeting = async (
roomId: number,
content: string,
): Promise<CreateDailyGreetingResponse> => {
try {
const requestBody: CreateDailyGreetingRequest = {
content,
};

const response = await apiClient.post<CreateDailyGreetingResponse>(
`/rooms/${roomId}/daily-greeting`,
requestBody,
);

return response.data;
} catch (error) {
console.error('오늘의 한마디 작성 API 오류:', error);
throw error;
}
};

/*
사용 예시:
try {
const result = await createDailyGreeting(1, "오늘도 좋은 하루 보내세요!");
if (result.isSuccess) {
console.log("오늘의 한마디 작성 성공:", result.data.attendanceCheckId);
// 성공 처리 로직 (예: 성공 메시지 표시, 페이지 새로고침 등)
} else {
console.error("오늘의 한마디 작성 실패:", result.message);
// 실패 처리 로직 (예: 에러 메시지 표시)
}
} catch (error) {
console.error("API 호출 오류:", error);
// 네트워크 에러 처리 로직
}
*/
27 changes: 22 additions & 5 deletions src/components/today-words/MessageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface MessageInputProps {
isReplying?: boolean;
onCancelReply?: () => void;
nickname?: string;
disabled?: boolean;
}

const MessageInput = ({
Expand All @@ -29,11 +30,13 @@ const MessageInput = ({
isReplying = false,
onCancelReply,
nickname,
disabled = false,
}: MessageInputProps) => {
const inputRef = useRef<HTMLTextAreaElement>(null);
const [isComposing, setIsComposing] = useState(false);

const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (disabled) return;
onChange(e.target.value);

if (inputRef.current) {
Expand All @@ -43,7 +46,7 @@ const MessageInput = ({
};

const handleSend = () => {
if (!value.trim()) return;
if (!value.trim() || disabled) return;
onSend();
onChange('');
if (inputRef.current) {
Expand All @@ -52,6 +55,7 @@ const MessageInput = ({
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (disabled) return;
if (e.key === 'Enter' && !e.shiftKey && !isComposing) {
e.preventDefault();
handleSend();
Expand Down Expand Up @@ -83,15 +87,28 @@ const MessageInput = ({
<MessageInputWrapper>
<StyledMessageInput
ref={inputRef}
placeholder={placeholder}
placeholder={disabled ? '전송 중...' : placeholder} // disabled일 때 placeholder 변경
value={value}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
onCompositionStart={() => !disabled && setIsComposing(true)} // disabled일 때는 composing 상태 변경 안함
onCompositionEnd={() => !disabled && setIsComposing(false)}
rows={1}
disabled={disabled}
style={{
opacity: disabled ? 0.6 : 1,
cursor: disabled ? 'not-allowed' : 'text',
}}
/>
<SendButton onClick={handleSend} disabled={!value.trim()} active={!!value.trim()}>
<SendButton
onClick={handleSend}
disabled={!value.trim() || disabled}
active={!!value.trim() && !disabled}
style={{
opacity: disabled ? 0.6 : 1,
cursor: disabled ? 'not-allowed' : 'pointer',
}}
>
<img src={sendIcon} alt="전송" />
</SendButton>
</MessageInputWrapper>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const Router = () => {
<Route path="otherfeed/:userId" element={<OtherFeedPage />} />
<Route path="follow/:type/:userId" element={<FollowerListPage />} />
<Route path="follow/:type" element={<FollowerListPage />} />
<Route path="today-words" element={<TodayWords />} />
<Route path="today-words/:roomId" element={<TodayWords />} />
<Route path="mypage" element={<Mypage />} />
<Route path="mypage/save" element={<SavePage />} />
<Route path="mypage/alert" element={<AlertPage />} />
Expand Down
150 changes: 117 additions & 33 deletions src/pages/today-words/TodayWords.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useState, useRef, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import TitleHeader from '../../components/common/TitleHeader';
import EmptyState from '../../components/today-words/EmptyState';
import MessageList from '../../components/today-words/MessageList/MessageList';
Expand All @@ -9,12 +9,17 @@ import leftarrow from '../../assets/common/leftArrow.svg';
import { Container, ContentArea } from './TodayWords.styled';
import type { Message } from '../../types/today';
import { dummyMessages } from '../../constants/today-constants';
import { createDailyGreeting } from '../../api/rooms/createDailyGreeting';
import { usePopupActions } from '../../hooks/usePopupActions';

const TodayWords = () => {
const navigate = useNavigate();
const { roomId } = useParams<{ roomId: string }>();
const messageListRef = useRef<MessageListRef>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const { openSnackbar } = usePopupActions();

// 개발용: 빈 상태와 글 있는 상태 토글
const [showMessages, setShowMessages] = useState(false);
Expand All @@ -23,45 +28,123 @@ const TodayWords = () => {
navigate(-1);
};

const handleSendMessage = () => {
if (inputValue.trim() === '') return;
const handleSendMessage = useCallback(async () => {
if (inputValue.trim() === '' || isSubmitting) return;

// 빈 상태에서 메시지를 보낼 때 실제 messages 상태를 업데이트
if (!showMessages) {
// 새 메시지 생성
const now = new Date();
const newMessage: Message = {
id: Date.now().toString(),
user: 'user.01',
content: inputValue.trim(),
timestamp: now
.toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
.replace(/\. /g, '.')
.replace(/\.$/, ''),
timeAgo: '방금 전',
createdAt: now,
};

// 실제 messages 상태에 추가
setMessages(prevMessages => [...prevMessages, newMessage]);
} else {
// MessageList의 addMessage 함수 호출 (더미 데이터 상태일 때)
if (messageListRef.current) {
messageListRef.current.addMessage(inputValue.trim());
// roomId가 없으면 에러 처리
if (!roomId) {
openSnackbar({
message: '방 정보를 찾을 수 없습니다.',
variant: 'top',
onClose: () => {},
});
return;
}

Comment on lines +31 to +43
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

roomId 존재 체크는 OK입니다. 숫자 유효성까지 함께 검증해 주세요.

현재는 roomId 문자열 존재만 확인합니다. 숫자가 아니거나(NaN), 0/음수인 경우를 걸러 API 호출을 막아야 합니다.

아래처럼 API 호출 직전에 유효성 검증을 추가하는 것을 권장합니다.

   try {
-      // API 호출 - 오늘의 한마디 작성
-      const response = await createDailyGreeting(parseInt(roomId), inputValue.trim());
+      // roomId 숫자 유효성 검증
+      const numericRoomId = Number(roomId);
+      if (!Number.isInteger(numericRoomId) || numericRoomId <= 0) {
+        openSnackbar({
+          message: '유효하지 않은 방 정보입니다.',
+          variant: 'top',
+          onClose: () => {},
+        });
+        return;
+      }
+
+      // API 호출 - 오늘의 한마디 작성
+      const response = await createDailyGreeting(numericRoomId, inputValue.trim());
🤖 Prompt for AI Agents
In src/pages/today-words/TodayWords.tsx around lines 31 to 43, the current check
only verifies roomId exists but doesn’t validate it’s a positive number; add a
numeric validation right before the API call by converting roomId to a number
(e.g., Number or parseInt), check for NaN and that the value is greater than 0,
and if invalid call openSnackbar with an appropriate error message and return to
prevent the API request; ensure you keep the existing empty/onClose behavior and
isSubmitting guard intact.

setIsSubmitting(true);

try {
// API 호출 - 오늘의 한마디 작성
const response = await createDailyGreeting(parseInt(roomId), inputValue.trim());

if (response.isSuccess) {
// 성공 시 새 메시지 생성
const now = new Date();
const newMessage: Message = {
id: response.data.attendanceCheckId.toString(),
user: 'user.01', // TODO: 실제 사용자 정보로 변경
content: inputValue.trim(),
timestamp: now
.toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
.replace(/\. /g, '.')
.replace(/\.$/, ''),
timeAgo: '방금 전',
createdAt: now,
};

// 실제 messages 상태에 추가
setMessages(prevMessages => [...prevMessages, newMessage]);

// 입력 필드 초기화
setInputValue('');

// 성공 메시지 표시
openSnackbar({
message: '오늘의 한마디가 작성되었습니다.',
variant: 'top',
onClose: () => {},
});

// 자동으로 스크롤을 아래로 이동
setTimeout(() => {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}, 100);
} else {
// API 에러 응답 처리
openSnackbar({
message: response.message || '오늘의 한마디 작성에 실패했습니다.',
variant: 'top',
onClose: () => {},
});
}
} catch (error) {
console.error('오늘의 한마디 작성 오류:', error);

// 에러 타입에 따른 메시지 처리
let errorMessage = '오늘의 한마디 작성 중 오류가 발생했습니다.';

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

if (axiosError.response?.data?.message) {
errorMessage = axiosError.response.data.message;
} else if (axiosError.response?.data?.code === 400) {
errorMessage = '오늘의 한마디 작성 가능 횟수를 초과했습니다.';
} else if (axiosError.response?.data?.code === 403) {
errorMessage = '방 접근 권한이 없습니다.';
} else if (axiosError.response?.data?.code === 404) {
errorMessage = '존재하지 않는 방입니다.';
}
}

openSnackbar({
message: errorMessage,
variant: 'top',
onClose: () => {},
});
} finally {
setIsSubmitting(false);
}
}, [inputValue, roomId, isSubmitting, openSnackbar]);

// 더미 모드에서 메시지 전송 처리 (개발용)
const handleDummySendMessage = useCallback(() => {
if (inputValue.trim() === '') return;

if (messageListRef.current) {
messageListRef.current.addMessage(inputValue.trim());
}
setInputValue('');

// 자동으로 스크롤을 아래로 이동
setTimeout(() => {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}, 100);
};
}, [inputValue]);

// 최종 메시지 전송 핸들러
const finalHandleSendMessage = showMessages ? handleDummySendMessage : handleSendMessage;

// MessageList에서 메시지가 삭제되었을 때 호출될 콜백
const handleMessageDelete = (messageId: string) => {
Expand Down Expand Up @@ -97,8 +180,9 @@ const TodayWords = () => {
<MessageInput
value={inputValue}
onChange={setInputValue}
onSend={handleSendMessage}
placeholder="메이트들과 간단한 인사를 나눠보세요!"
onSend={finalHandleSendMessage}
placeholder="메이트들과 간단한 인사를 나눠 보세요!"
disabled={isSubmitting}
/>

{/* 개발용 토글 버튼 */}
Expand Down