Skip to content

feat: 오늘의 한마디 조회 API 연동 구현#153

Merged
ljh130334 merged 8 commits into
developfrom
feat/api-rooms-today
Aug 18, 2025
Merged

feat: 오늘의 한마디 조회 API 연동 구현#153
ljh130334 merged 8 commits into
developfrom
feat/api-rooms-today

Conversation

@ljh130334
Copy link
Copy Markdown
Member

@ljh130334 ljh130334 commented Aug 18, 2025

#️⃣ 연관된 이슈

#106

📝 작업 내용

이번 PR에서는 오늘의 한마디 기능의 조회 API를 완전히 연동하고, 사용자 경험을 개선하는 다양한 기능들을 구현했습니다.

🕸️ 주요 작업 내용

1. 오늘의 한마디 조회 API 연동

  • getDailyGreeting API 함수를 새로 생성하여 백엔드 API와 연동했습니다.
  • API 응답 데이터를 기존 Message 타입과 호환되도록 변환하는 로직을 구현했습니다.

2. 무한 스크롤 및 페이지네이션 구현

  • nextCursor 기반의 커서 페이지네이션을 구현하여 대용량 데이터를 효율적으로 로드할 수 있게 했습니다.
  • 스크롤 이벤트를 감지하여 자동으로 다음 페이지 데이터를 불러오는 무한 스크롤 기능을 추가했습니다.
  • 로딩 상태 관리를 통해 사용자에게 명확한 피드백을 제공합니다.

3. 사용자 인터페이스 개선

  • 메시지 정렬 순서를 수정하여 채팅앱처럼 아래로 갈수록 최신 메시지가 표시되도록 했습니다.
  • 프로필 이미지 지원을 추가하여 API에서 제공하는 creatorProfileImageUrl을 활용합니다.
  • isWriter 필드를 활용하여 내가 쓴 메시지와 다른 사람의 메시지를 정확히 구분합니다.

4. 메시지 관리 기능 개선

  • 내가 쓴 메시지에는 "삭제하기" 옵션을, 다른 사람의 메시지에는 "신고하기" 옵션을 표시하도록 구현했습니다.
  • 삭제 API가 아직 개발되지 않은 상황을 고려하여 임시 알림 처리를 했습니다.

5. 하루 5개 작성 제한 로직 구현

  • 사용자가 하루에 최대 5개까지만 오늘의 한마디를 작성할 수 있도록 제한하는 로직을 구현했습니다.
  • 5번째 메시지 작성 완료 시 "오늘의 한마디는 하루에 다섯번까지 작성할 수 있어요"라는 정보성 토스트를 표시합니다.
  • 6번째 작성 시도 시 같은 메시지로 작성을 차단하고 토스트를 표시합니다.

6. 타입 안정성 및 에러 처리 강화

  • TypeScript 타입 정의를 개선하여 profileImageUrl, isWriter 등의 새로운 필드를 추가했습니다.
  • API 에러 상황별로 적절한 메시지를 표시하도록 에러 처리를 강화했습니다.
  • timeAgo 표시 오류를 수정하여 "NaN일 전" 대신 정확한 시간 정보를 표시합니다.

기술적 구현 세부사항

  • API 응답의 postDate 필드가 이미 계산된 상대 시간 문자열임을 파악하고 이를 그대로 활용
  • 메시지 정렬을 attendanceCheckId 기준으로 변경하여 정확한 시간순 정렬 구현
  • 로딩 상태 관리를 통한 사용자 경험 개선 (초기 로딩, 추가 로딩 구분)
  • 오늘 날짜 계산 로직을 통한 정확한 일일 제한 카운팅

Summary by CodeRabbit

  • New Features
    • 오늘의 한마디 실데이터 연동 및 무한 스크롤 지원
    • 초기/추가 로딩 스피너와 빈 상태 화면 제공
    • 일일 작성 제한(하루 5회) 적용 및 스낵바 안내
    • 접근 권한/방 없음/제한 초과 등 상황별 오류 메시지 표시
  • Style
    • 아바타에 프로필 이미지 표시(배경 이미지, 크기/정렬 최적화)
  • Changes
    • 메시지 정렬을 시간 흐름에 맞게 개선하여 일관된 표시
    • 삭제 기능은 준비 중으로 안내(추후 제공 예정)

  - getDailyGreeting API 함수 생성 (src/api/rooms/getDailyGreeting.ts)
  - TodayWords 컴포넌트에 실제 API 데이터 연동
  - 무한 스크롤 및 페이지네이션 구현 (nextCursor 기반)
  - 로딩 상태 및 에러 처리 추가
  - 프로필 이미지 지원 기능 추가
  - 타입 정의 개선 (Message 인터페이스에 profileImageUrl, isWriter 필드 추가)
  - 새 메시지 작성 후 자동 새로고침 기능
  - 개발용 토글 버튼으로 실제/더미 데이터 전환 가능
@vercel
Copy link
Copy Markdown

vercel Bot commented Aug 18, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
thip Ready Ready Preview Comment Aug 18, 2025 6:34am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 18, 2025

Walkthrough

방 생성의 “오늘의 인사” 데이터를 불러오기 위한 GET API 래퍼를 추가하고, 관련 타입을 정의했습니다. TodayWords 페이지는 해당 API를 사용해 초기 로드와 무한 스크롤 로드를 구현했습니다. 메시지/아바타 렌더링 로직과 타입을 갱신했으며, 일일 전송 제한 및 오류 처리 분기를 추가했습니다.

Changes

Cohort / File(s) Change summary
Daily Greeting API
src/api/rooms/getDailyGreeting.ts
새로운 API 래퍼 추가: getDailyGreeting({ roomId, cursor? }) → DailyGreetingResponse. 쿼리 파라미터 조합, apiClient.get 호출, data 반환, 오류 로깅 및 재throw.
Today Types
src/types/today.ts
Message에 profileImageUrl?, isWriter? 추가. TodayCommentItem, DailyGreetingData 타입 신설(목록+페이지네이션 메타).
TodayWords Page
src/pages/today-words/TodayWords.tsx
getDailyGreeting 연동, 초기/추가 로딩 상태, 무한 스크롤, API 결과를 Message로 변환, 일일 제한(5) 로직, 에러 코드별 메시지 처리, 빈 상태/로딩 UI 분기.
MessageList Component
src/components/today-words/MessageList/MessageList.tsx, src/components/today-words/MessageList/MessageList.styled.ts
정렬 기준을 id 오름차순으로 변경, isWriter로 “내 메시지” 판별, 삭제 로직 비활성화(플레이스홀더), 아바타에 profileImageUrl 전달/스타일 추가(background-image, cover/center).

Sequence Diagram(s)

sequenceDiagram
  participant U as User
  participant TW as TodayWords Page
  participant API as getDailyGreeting()
  participant AC as apiClient
  participant S as Server

  U->>TW: 페이지 진입
  TW->>API: getDailyGreeting({roomId, cursor?})
  API->>AC: GET /rooms/{roomId}/daily-greeting
  AC->>S: HTTP GET
  S-->>AC: 200 { todayCommentList, nextCursor, isLast }
  AC-->>API: response.data
  API-->>TW: DailyGreetingData
  TW->>TW: 변환(TodayCommentItem → Message)
  TW-->>U: MessageList 렌더/빈 상태/로딩 표시
Loading
sequenceDiagram
  participant U as User Scrolls
  participant TW as TodayWords Page
  participant API as getDailyGreeting()
  participant AC as apiClient
  participant S as Server

  U->>TW: 바닥 근처 스크롤
  TW->>TW: isLast 확인 / 중복 로드 방지
  TW->>API: getDailyGreeting({roomId, cursor: nextCursor})
  API->>AC: GET /rooms/{roomId}/daily-greeting
  AC->>S: HTTP GET
  S-->>AC: 200 { todayCommentList, nextCursor, isLast }
  AC-->>API: response.data
  API-->>TW: DailyGreetingData
  TW->>TW: 메시지 추가/상태 갱신
  TW-->>U: 리스트 하단 갱신/로딩 스피너 종료
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Suggested labels

✨ Feature, 📬 API

Suggested reviewers

  • ho0010

Poem

오늘의 인사, 콩콩 뛰어 모았지요 🐇
커서 물고 살짝 더, 리스트는 쑥쑥 자라요
아바타엔 미소 한 장, URL로 반짝반짝 ✨
한 날 다섯 번, 약속은 꼭 지켜요
스크롤 끝에 또 만나요—안녕, 내일도 인사! 🌤️

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/api-rooms-today

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (11)
src/types/today.ts (2)

12-22: API-UI 매핑 용어 확인 요청 (date vs timestamp)

TodayCommentItem.dateMessage.timestamp로 그대로 매핑됩니다. TodayWords에서 일자 비교는 toLocaleDateString('ko-KR') 포맷(YYYY.MM.DD)을 사용하므로, 백엔드에서 내려주는 date 포맷이 동일한지 확인 필요합니다. 불일치 시 일일 전송 횟수 계산이 오작동할 수 있습니다.


24-27: 중복 타입 정의 제거 제안

DailyGreetingData를 정의했지만 API 모듈(getDailyGreeting.ts)에서는 동일 구조를 인라인으로 재정의하고 있습니다. 단일 소스 타입으로 재사용하면 유지보수성이 좋아집니다.

다음과 같이 API 모듈에서 재사용을 제안합니다:

- import { type TodayCommentItem } from '../../types/today';
+ import { type TodayCommentItem, type DailyGreetingData } from '../../types/today';

- export interface DailyGreetingResponse {
+ export interface DailyGreetingResponse {
   isSuccess: boolean;
   code: number;
   message: string;
-  data: {
-    todayCommentList: TodayCommentItem[];
-    nextCursor: string;
-    isLast: boolean;
-  };
+  data: DailyGreetingData;
 }
src/components/today-words/MessageList/MessageList.styled.ts (1)

28-39: 아바타 이미지 렌더링 추가(LGTM) + URL 인용(quoting) 권장

Background 이미지 렌더링은 좋습니다. 다만 URL에 특수문자가 포함될 때 CSS 파싱 이슈를 줄이기 위해 따옴표로 감싸는 것을 권장합니다.

다음과 같이 수정을 제안합니다:

-  background-image: ${props => props.profileImageUrl ? `url(${props.profileImageUrl})` : 'none'};
+  background-image: ${props => props.profileImageUrl ? `url("${props.profileImageUrl}")` : 'none'};
src/components/today-words/MessageList/MessageList.tsx (1)

101-106: alert 대신 일관된 토스트/스낵바 사용 권장

전역 UI 피드백 패턴과 일관성을 위해 alert 대신 상위의 스낵바/바텀시트를 재사용하는 방식을 권장합니다. 추후 삭제 API 연동 시에도 동일 UI 패턴을 사용할 수 있습니다.

src/api/rooms/getDailyGreeting.ts (1)

5-14: 응답 타입 정의의 단일화 제안

여기서 응답 data 구조를 인라인으로 재정의하는 대신 DailyGreetingData 타입을 재사용하면 타입 드리프트를 방지할 수 있습니다. (동일 제안은 types 파일 코멘트에 상세)

src/pages/today-words/TodayWords.tsx (6)

19-19: 사용하지 않는 ref 및 타입 정리 제안

messageListRef와 관련 타입 import가 현재 사용되지 않습니다. 불필요한 코드 제거로 가독성과 번들 크기를 소폭 개선할 수 있습니다.

다음과 같이 제거를 제안합니다:

- import type { MessageListRef } from '../../components/today-words/MessageList/MessageList';
+ // (unused) MessageListRef import 제거

- const messageListRef = useRef<MessageListRef>(null);
+ // (unused) ref 제거

-              <MessageList
-                ref={messageListRef}
-                messages={messages}
-              />
+              <MessageList messages={messages} />

Also applies to: 296-299, 6-7


34-45: 일일 작성 수 계산의 포맷 의존성

클라이언트에서 생성한 today 문자열과 서버 item.date 포맷이 다르면 카운트가 틀어질 수 있습니다. 안전하게 하려면:

  • 서버가 YYYY.MM.DD로 내려주도록 계약을 명시하거나,
  • item.date를 Date로 파싱 후 동일 포맷 함수로 변환해 비교,
  • 혹은 서버에서 오늘 작성 개수를 별도 필드로 내려받아 사용하는 방식을 고려해 주세요.

85-89: parseInt에 radix 명시 권장

암묵적 10진수에 의존하지 말고 기수(10)를 명시해 안전성을 높이세요.

-        roomId: parseInt(roomId),
+        roomId: parseInt(roomId, 10),

75-84: 의존성 배열 보강으로 stale 클로저 방지

loadMessages의 의존성에 openSnackbar가 누락되어 있고, 이를 사용하는 훅/콜백에서도 loadMessages가 의존성에서 빠져 있습니다. 훅 규칙을 준수하면 재렌더/파라미터 변경 시 최신 함수를 안전하게 참조할 수 있습니다.

다음과 같이 수정하세요:

-  const loadMessages = useCallback(async (cursor?: string, isRefresh = false) => {
+  const loadMessages = useCallback(async (cursor?: string, isRefresh = false) => {
     // ...
-  }, [roomId]);
+  }, [roomId, openSnackbar]);

-  const loadMoreMessages = useCallback(() => {
+  const loadMoreMessages = useCallback(() => {
     if (!isLoadingMore && !isLast && nextCursor && roomId) {
       loadMessages(nextCursor);
     }
-  }, [isLoadingMore, isLast, nextCursor, roomId]);
+  }, [isLoadingMore, isLast, nextCursor, roomId, loadMessages]);

-  useEffect(() => {
+  useEffect(() => {
     if (roomId && !hasInitiallyLoaded) {
       loadMessages(undefined, true);
     }
-  }, [roomId, hasInitiallyLoaded]);
+  }, [roomId, hasInitiallyLoaded, loadMessages]);

Also applies to: 142-143


158-171: 무한 스크롤 호출 제어 강화 제안

현재 스크롤 이벤트는 연속으로 트리거될 수 있어 미세한 중복 호출 여지가 있습니다. 실제 문제는 크지 않지만, 성능/안정성을 위해 다음을 고려해 주세요:

  • 스크롤 핸들러에 throttle/debounce 적용,
  • 또는 IntersectionObserver로 하단 센티널 관찰.

Also applies to: 145-149


186-195: 클라이언트 측 일일 제한 UX 보완 포인트

서버에서 400으로 차단하는 로직이 이미 있으므로, 클라이언트 측 선제 차단은 UX용 보조 장치입니다. 단, 현재 목록이 완전하지 않으면(스크롤로 더 불러오지 않은 경우) 과소/과대 차단될 수 있습니다. 가능하다면 서버에서 “오늘 내가 작성한 개수”를 추가로 내려받아 사용해 정확도를 높여 주세요.

Also applies to: 255-263

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 849a63d and 8df11e1.

📒 Files selected for processing (5)
  • src/api/rooms/getDailyGreeting.ts (1 hunks)
  • src/components/today-words/MessageList/MessageList.styled.ts (1 hunks)
  • src/components/today-words/MessageList/MessageList.tsx (5 hunks)
  • src/pages/today-words/TodayWords.tsx (6 hunks)
  • src/types/today.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/components/today-words/MessageList/MessageList.styled.ts (1)
src/styles/global/global.ts (1)
  • semanticColors (80-153)
src/api/rooms/getDailyGreeting.ts (2)
src/types/today.ts (1)
  • TodayCommentItem (13-22)
src/api/index.ts (1)
  • apiClient (7-14)
src/pages/today-words/TodayWords.tsx (6)
src/types/today.ts (2)
  • TodayCommentItem (13-22)
  • Message (1-10)
src/api/rooms/getDailyGreeting.ts (1)
  • getDailyGreeting (22-33)
src/api/rooms/createDailyGreeting.ts (1)
  • createDailyGreeting (22-41)
src/components/today-words/MessageList/MessageList.styled.ts (1)
  • MessageList (4-7)
src/pages/today-words/TodayWords.styled.ts (1)
  • ContentArea (17-22)
src/components/today-words/MessageInput.styled.ts (1)
  • MessageInput (62-90)
src/components/today-words/MessageList/MessageList.tsx (1)
src/components/today-words/MessageList/MessageList.styled.ts (1)
  • UserAvatar (28-39)
🔇 Additional comments (6)
src/types/today.ts (1)

8-10: Message 타입 확장(LGTM)

profileImageUrl?, isWriter? 추가로 UI 요구사항을 잘 반영했습니다. 하위 호환성도 유지됩니다.

src/components/today-words/MessageList/MessageList.tsx (2)

115-117: 내 메시지 판별 변경(LGTM)

isWriter를 통한 판별은 서버 신뢰 모델과 맞고, 닉네임/ID 비교보다 안전합니다.


131-132: 아바타 prop 전달(LGTM)

profileImageUrl 전달로 스타일 컴포넌트와 타입 확장이 잘 연결되었습니다.

src/api/rooms/getDailyGreeting.ts (1)

22-33: API 호출 래퍼 기본기 충실(LGTM)

  • 선택적 cursor 파라미터 처리 적절
  • 에러 로깅 후 재전파로 상위에서 컨텍스트별 처리 가능
src/pages/today-words/TodayWords.tsx (2)

53-72: API→UI 매핑(LGTM) + 포맷 합의 확인

매핑 자체는 적절합니다. 다만 timestamp: item.date가 아래 일일 제한 계산의 날짜 문자열(ko-KR 포맷)과 동일한지 확인 필요합니다. 불일치 시 같은 날 메시지가 누락되어 5회 제한 검증이 부정확해질 수 있습니다.


288-305: 로딩 상태 UI(LGTM)

초기 로딩/추가 로딩 상태 분리가 명확하고, 스피너 위치도 적절합니다.

Comment on lines +73 to 76
// 먼저 모든 메시지를 시간순으로 정렬 (아래로 올수록 최신)
// ID를 기준으로 정렬 (ID가 클수록 최신)
const sortedMessages = messages.sort((a, b) => parseInt(a.id) - parseInt(b.id));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

상태 배열을 직접 변형하는 정렬 로직(치명적) — 불변성 위반

messages.sort(...)는 state 배열을 제자리(in-place)에서 변형합니다. 이 컴포넌트는 상위에서 받은 배열 참조(initialMessages)를 그대로 state에 저장하므로, 상위 컴포넌트의 state까지 의도치 않게 변형될 수 있습니다. 이는 예측 불가능한 렌더링 버그를 유발합니다.

다음과 같이 복사 후 정렬하도록 수정하세요. 함께 parseInt에 radix 지정 및 NaN 대비도 권장합니다.

-    const sortedMessages = messages.sort((a, b) => parseInt(a.id) - parseInt(b.id));
+    const sortedMessages = [...messages].sort((a, b) => {
+      const aNum = parseInt(a.id, 10);
+      const bNum = parseInt(b.id, 10);
+      if (Number.isNaN(aNum) || Number.isNaN(bNum)) {
+        // 숫자 변환 실패 시 createdAt으로 폴백(오래된 → 최신)
+        return a.createdAt.getTime() - b.createdAt.getTime();
+      }
+      return aNum - bNum; // ID가 클수록 최신 → 오름차순 정렬 시 아래로 갈수록 최신
+    });
📝 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
// 먼저 모든 메시지를 시간순으로 정렬 (아래로 올수록 최신)
// ID를 기준으로 정렬 (ID가 클수록 최신)
const sortedMessages = messages.sort((a, b) => parseInt(a.id) - parseInt(b.id));
// 먼저 모든 메시지를 시간순으로 정렬 (아래로 올수록 최신)
// ID를 기준으로 정렬 (ID가 클수록 최신)
const sortedMessages = [...messages].sort((a, b) => {
const aNum = parseInt(a.id, 10);
const bNum = parseInt(b.id, 10);
if (Number.isNaN(aNum) || Number.isNaN(bNum)) {
// 숫자 변환 실패 시 createdAt으로 폴백(오래된 → 최신)
return a.createdAt.getTime() - b.createdAt.getTime();
}
return aNum - bNum; // ID가 클수록 최신 → 오름차순 정렬 시 아래로 갈수록 최신
});
🤖 Prompt for AI Agents
In src/components/today-words/MessageList/MessageList.tsx around lines 73 to 76,
the code calls messages.sort(...) which mutates the original state array; make a
non-mutating copy first (e.g., use [...messages] or Array.from(messages]) and
sort that copy, and replace usages to use the sorted copy; also call parseInt
with an explicit radix (parseInt(x, 10)) and guard against NaN by providing a
numeric fallback (e.g., Number(x) || 0 or isNaN checks) when computing the sort
keys.

@ljh130334 ljh130334 merged commit b0816f1 into develop Aug 18, 2025
3 checks passed
@ljh130334 ljh130334 deleted the feat/api-rooms-today branch September 1, 2025 03:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant