feat: 오늘의 한마디 삭제 API 연결#232
Conversation
Walkthrough토큰 부재 시 하드코딩된 Authorization 토큰을 주입하도록 요청 인터셉터와 Feed 초기화 로직을 수정. 새로운 방 단위 "오늘의 한마디" 삭제 API를 추가하고, MessageList/TodayWords에 삭제 흐름을 연동하며 roomId와 삭제 콜백을 전달해 상태를 동기화. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as 사용자
participant ML as MessageList
participant API as apiClient
participant S as 서버
participant TW as TodayWords(State)
User->>ML: 메시지 삭제 클릭
ML->>ML: selectedMessageId 확인, roomId 확인
alt roomId 있음
ML->>API: DELETE /rooms/{roomId}/daily-greeting/{attendanceCheckId}
API->>S: 요청 (Authorization 포함)
S-->>API: 응답 { isSuccess, data.roomId ... }
alt 성공
API-->>ML: 응답 데이터
ML->>TW: onMessageDelete(messageId)
TW->>TW: 로컬 messages 상태에서 제거
ML->>User: 스낵바(삭제 완료)
else 실패
API-->>ML: 오류/실패 메시지
ML->>User: 스낵바(실패 메시지)
end
else roomId 없음
ML->>User: 스낵바(방 정보 없음)
end
ML->>ML: 선택 해제
sequenceDiagram
autonumber
participant FE as Feed 초기화
participant LS as localStorage
participant AX as axios interceptor
FE->>LS: authToken 조회
alt 없음
FE->>LS: 하드코딩 토큰 저장
note right of FE: 이후 요청부터 토큰 사용
else 있음
FE-->>FE: 기존 흐름 유지
end
AX->>LS: 요청 시 authToken 확인
alt 없음
AX-->>AX: Authorization에 하드코딩 토큰 설정
else 있음
AX-->>AX: Authorization에 저장된 토큰 설정
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
src/api/index.ts (1)
38-47: 401 전역 리다이렉트는 신중히: 라우터 네비게이션/재발급 흐름 고려.window.location으로 즉시 루트 이동은 SPA에서 사용자 경험을 해칠 수 있고, 401이 백그라운드 폴링 등에서 발생 시 불필요한 풀 리로드로 이어집니다.
- 권장: 401 → (1) 토큰 재발급 시도 → 실패 시 (2) 토큰/세션 정리 후 라우터로 로그인 이동.
- 최소한 DEV/PROD 분기 또는 특정 엔드포인트만 리다이렉트하도록 범위를 축소하세요.
src/pages/feed/Feed.tsx (1)
159-166: 하드코딩 토큰 사용 자제 및 중복 소스 통합.api/index.ts와 본 파일 모두에서 토큰을 별도로 주입하면 관리 지점이 늘고 불일치가 발생합니다. 토큰 주입/재발급/삭제는 한 곳(api 레이어 인터셉터)에서만 처리하고, 페이지는 그 결과만 소비하도록 역할을 분리해 주세요.
src/pages/today-words/TodayWords.tsx (1)
325-327: parseInt 기수 명시 권장.기수 미지정 시 일부 런타임에서 예외 동작 여지가 있습니다. 10진수 기수를 명시하세요.
- roomId={roomId ? parseInt(roomId) : undefined} + roomId={roomId ? parseInt(roomId, 10) : undefined}src/api/rooms/deleteDailyGreeting.ts (1)
24-28: 에러 표면화 방식 개선(선택).현재 콘솔 로깅 후 원본 throw는 적절합니다. 필요 시 AxiosError를 제네릭으로 타이핑해 서버 메시지/코드 추출을 표준화하거나, 공통 에러 유틸로 위임하는 것을 고려해 주세요.
- } catch (error) { - console.error('오늘의 한마디 삭제 API 오류:', error); - throw error; - } + } catch (error) { + console.error('오늘의 한마디 삭제 API 오류:', error); + // 예: 공통 에러 핸들러로 위임하여 메시지 정규화 + // throw normalizeApiError(error); + throw error; + }src/components/today-words/MessageList/MessageList.tsx (1)
81-82: 정렬 시 상태 배열을 직접 변형하지 않도록 방어.messages.sort는 state를 직접 mutate합니다. 불변성 유지와 불필요한 리렌더 방지를 위해 사본 정렬 또는 useMemo를 사용하세요.
- const sortedMessages = messages.sort((a, b) => parseInt(a.id) - parseInt(b.id)); + const sortedMessages = [...messages].sort((a, b) => Number(a.id) - Number(b.id));
📜 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.
📒 Files selected for processing (5)
src/api/index.ts(1 hunks)src/api/rooms/deleteDailyGreeting.ts(1 hunks)src/components/today-words/MessageList/MessageList.tsx(4 hunks)src/pages/feed/Feed.tsx(1 hunks)src/pages/today-words/TodayWords.tsx(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/api/rooms/deleteDailyGreeting.ts (1)
src/api/index.ts (1)
apiClient(7-14)
src/components/today-words/MessageList/MessageList.tsx (1)
src/api/rooms/deleteDailyGreeting.ts (1)
deleteDailyGreeting(14-28)
🔇 Additional comments (2)
src/pages/today-words/TodayWords.tsx (1)
299-304: 로컬 상태 동기화 콜백 추가 좋습니다.자식/부모 간 삭제 동기화를 명확히 해 UI 일관성이 확보됩니다. 현재 구현은 충분합니다.
src/api/rooms/deleteDailyGreeting.ts (1)
14-23: DELETE API 래퍼 구현 깔끔합니다.타이핑, 경로 구성, data 반환까지 일관적입니다.
| config.headers.Authorization = | ||
| 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjksImlhdCI6MTc1Njc4MjEyNywiZXhwIjoxNzU5Mzc0MTI3fQ.iRU7rN90Vs9Wykxvw-gkyAkbyB-HQENm_WifYHb2UR8eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjksImlhdCI6MTc1Njc4NTY5NSwiZXhwIjoxNzU5Mzc3Njk1fQ.jnYVdrvtHivfyteXPHAZmAM1mkwW2U66EPn7BylzHu0'; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
하드코딩된 Bearer 토큰 제거 필요 (보안·운영 리스크 + 토큰 문자열 손상 가능성).
- 레포지토리에 토큰을 하드코딩하면 유출 위험이 큽니다(XSS/리포 노출/로그 수집 등).
- 현재 값은 JWT가 2개 연속으로 붙은 형태로 보이며(중복 서명부 존재), 실제 인증 실패를 유발할 수 있습니다.
- 토큰이 없을 때는 헤더를 비우고(혹은 DEV 환경에서만 .env의 임시 토큰 사용) 401 흐름에서만 로그인/재발급로 이어지도록 해주세요.
아래처럼 DEV 한정 .env 토큰만 주입(선택)하고, 없으면 Authorization을 제거하는 쪽으로 정리하는 것을 권장합니다.
- } else {
- console.log('❌ localStorage에 토큰이 없습니다.');
- config.headers.Authorization =
- 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjksImlhdCI6MTc1Njc4MjEyNywiZXhwIjoxNzU5Mzc0MTI3fQ.iRU7rN90Vs9Wykxvw-gkyAkbyB-HQENm_WifYHb2UR8eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjksImlhdCI6MTc1Njc4NTY5NSwiZXhwIjoxNzU5Mzc3Njk1fQ.jnYVdrvtHivfyteXPHAZmAM1mkwW2U66EPn7BylzHu0';
- }
+ } else {
+ console.warn('❌ localStorage에 토큰이 없습니다.');
+ // DEV 환경에서만 .env 토큰 사용 (선택)
+ if (import.meta.env.DEV && import.meta.env.VITE_DEV_AUTH_TOKEN) {
+ config.headers.Authorization = `Bearer ${import.meta.env.VITE_DEV_AUTH_TOKEN}`;
+ } else {
+ delete config.headers.Authorization;
+ }
+ }📝 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.
| config.headers.Authorization = | |
| 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjksImlhdCI6MTc1Njc4MjEyNywiZXhwIjoxNzU5Mzc0MTI3fQ.iRU7rN90Vs9Wykxvw-gkyAkbyB-HQENm_WifYHb2UR8eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjksImlhdCI6MTc1Njc4NTY5NSwiZXhwIjoxNzU5Mzc3Njk1fQ.jnYVdrvtHivfyteXPHAZmAM1mkwW2U66EPn7BylzHu0'; | |
| if (token) { | |
| config.headers.Authorization = `Bearer ${token}`; | |
| } else { | |
| console.warn('❌ localStorage에 토큰이 없습니다.'); | |
| // DEV 환경에서만 .env 토큰 사용 (선택) | |
| if (import.meta.env.DEV && import.meta.env.VITE_DEV_AUTH_TOKEN) { | |
| config.headers.Authorization = `Bearer ${import.meta.env.VITE_DEV_AUTH_TOKEN}`; | |
| } else { | |
| delete config.headers.Authorization; | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/api/index.ts around lines 27-28, remove the hardcoded Bearer token (it
currently contains two concatenated JWTs) and instead read an optional token
from environment only in DEV (e.g., process.env.DEV_BEARER_TOKEN); if no token
is available delete/omit config.headers.Authorization so requests proceed
without an Authorization header and let the 401 handling trigger login/refresh;
ensure no token string remains in repo, and add a clear comment that production
tokens must never be hardcoded and should come from secure storage.
| const handleDelete = async () => { | ||
| if (selectedMessageId && roomId) { | ||
| try { | ||
| // API에서는 attendanceCheckId가 필요하므로 selectedMessageId를 사용 | ||
| const attendanceCheckId = parseInt(selectedMessageId); | ||
|
|
||
| const result = await deleteDailyGreeting(roomId, attendanceCheckId); | ||
|
|
||
| if (result.isSuccess) { | ||
| // 로컬 상태에서 메시지 제거 | ||
| setMessages(prevMessages => | ||
| prevMessages.filter(msg => msg.id !== selectedMessageId) | ||
| ); | ||
|
|
||
| // 부모 컴포넌트에 삭제 알림 | ||
| if (onMessageDelete) { | ||
| onMessageDelete(selectedMessageId); | ||
| } | ||
|
|
||
| openSnackbar({ | ||
| message: '오늘의 한마디가 삭제되었습니다.', | ||
| variant: 'top', | ||
| isError: false, | ||
| onClose: () => {}, | ||
| }); | ||
| } else { | ||
| openSnackbar({ | ||
| message: result.message || '삭제에 실패했습니다.', | ||
| variant: 'top', | ||
| isError: true, | ||
| onClose: () => {}, | ||
| }); | ||
| } | ||
| } catch (error) { | ||
| console.error('삭제 오류:', error); | ||
| openSnackbar({ | ||
| message: '삭제 중 오류가 발생했습니다.', | ||
| variant: 'top', | ||
| isError: true, | ||
| onClose: () => {}, | ||
| }); | ||
| } | ||
| } else if (!roomId) { |
There was a problem hiding this comment.
🛠️ Refactor suggestion
삭제 처리에 NaN 가드 및 중복 클릭 방지 추가 권장.
- selectedMessageId 파싱 실패 시(비정상 id) API 경로가
/NaN이 됩니다. - 중복 클릭으로 다중 요청이 발생할 수 있습니다(낙관적 업데이트와 경합).
간단 가드와 진행 상태 플래그를 추가해 주세요.
+ const [deleting, setDeleting] = useState(false);
@@
- const handleDelete = async () => {
- if (selectedMessageId && roomId) {
+ const handleDelete = async () => {
+ if (deleting) return;
+ if (selectedMessageId && roomId) {
try {
- const attendanceCheckId = parseInt(selectedMessageId);
+ const attendanceCheckId = Number(selectedMessageId);
+ if (Number.isNaN(attendanceCheckId)) {
+ openSnackbar({ message: '잘못된 메시지 ID 입니다.', variant: 'top', isError: true, onClose: () => {} });
+ return;
+ }
- const result = await deleteDailyGreeting(roomId, attendanceCheckId);
+ setDeleting(true);
+ const result = await deleteDailyGreeting(roomId, attendanceCheckId);
@@
- } catch (error) {
+ } catch (error) {
console.error('삭제 오류:', error);
openSnackbar({
message: '삭제 중 오류가 발생했습니다.',
variant: 'top',
isError: true,
onClose: () => {},
});
- }
+ } finally {
+ setDeleting(false);
+ }
} else if (!roomId) {📝 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.
| const handleDelete = async () => { | |
| if (selectedMessageId && roomId) { | |
| try { | |
| // API에서는 attendanceCheckId가 필요하므로 selectedMessageId를 사용 | |
| const attendanceCheckId = parseInt(selectedMessageId); | |
| const result = await deleteDailyGreeting(roomId, attendanceCheckId); | |
| if (result.isSuccess) { | |
| // 로컬 상태에서 메시지 제거 | |
| setMessages(prevMessages => | |
| prevMessages.filter(msg => msg.id !== selectedMessageId) | |
| ); | |
| // 부모 컴포넌트에 삭제 알림 | |
| if (onMessageDelete) { | |
| onMessageDelete(selectedMessageId); | |
| } | |
| openSnackbar({ | |
| message: '오늘의 한마디가 삭제되었습니다.', | |
| variant: 'top', | |
| isError: false, | |
| onClose: () => {}, | |
| }); | |
| } else { | |
| openSnackbar({ | |
| message: result.message || '삭제에 실패했습니다.', | |
| variant: 'top', | |
| isError: true, | |
| onClose: () => {}, | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('삭제 오류:', error); | |
| openSnackbar({ | |
| message: '삭제 중 오류가 발생했습니다.', | |
| variant: 'top', | |
| isError: true, | |
| onClose: () => {}, | |
| }); | |
| } | |
| } else if (!roomId) { | |
| // --- somewhere near the top of your component function --- | |
| const [deleting, setDeleting] = useState(false); | |
| const handleDelete = async () => { | |
| if (deleting) return; | |
| if (selectedMessageId && roomId) { | |
| try { | |
| // API에서는 attendanceCheckId가 필요하므로 selectedMessageId를 사용 | |
| const attendanceCheckId = Number(selectedMessageId); | |
| if (Number.isNaN(attendanceCheckId)) { | |
| openSnackbar({ | |
| message: '잘못된 메시지 ID 입니다.', | |
| variant: 'top', | |
| isError: true, | |
| onClose: () => {}, | |
| }); | |
| return; | |
| } | |
| setDeleting(true); | |
| const result = await deleteDailyGreeting(roomId, attendanceCheckId); | |
| if (result.isSuccess) { | |
| // 로컬 상태에서 메시지 제거 | |
| setMessages(prevMessages => | |
| prevMessages.filter(msg => msg.id !== selectedMessageId) | |
| ); | |
| // 부모 컴포넌트에 삭제 알림 | |
| if (onMessageDelete) { | |
| onMessageDelete(selectedMessageId); | |
| } | |
| openSnackbar({ | |
| message: '오늘의 한마디가 삭제되었습니다.', | |
| variant: 'top', | |
| isError: false, | |
| onClose: () => {}, | |
| }); | |
| } else { | |
| openSnackbar({ | |
| message: result.message || '삭제에 실패했습니다.', | |
| variant: 'top', | |
| isError: true, | |
| onClose: () => {}, | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('삭제 오류:', error); | |
| openSnackbar({ | |
| message: '삭제 중 오류가 발생했습니다.', | |
| variant: 'top', | |
| isError: true, | |
| onClose: () => {}, | |
| }); | |
| } finally { | |
| setDeleting(false); | |
| } | |
| } else if (!roomId) { | |
| // ... existing else-if body ... |
🤖 Prompt for AI Agents
In src/components/today-words/MessageList/MessageList.tsx around lines 107 to
149, add a guard and a debounce flag to prevent NaN and duplicate deletes:
validate that selectedMessageId parses to a finite integer (const
attendanceCheckId = Number.parseInt(selectedMessageId); if
Number.isNaN(attendanceCheckId) return with an error snackbar), introduce an
isDeleting state flag checked at the top to ignore re-entrant calls, set
isDeleting = true before the API call and clear it in a finally block, and when
updating local state and calling onMessageDelete compare using the same numeric
id type (e.g., msg.id !== attendanceCheckId) to avoid mismatches; also ensure
any UI delete button/trigger is disabled while isDeleting is true.
| localStorage.setItem( | ||
| 'authToken', | ||
| 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjksImlhdCI6MTc1Njc4MjEyNywiZXhwIjoxNzU5Mzc0MTI3fQ.iRU7rN90Vs9Wykxvw-gkyAkbyB-HQENm_WifYHb2UR8', | ||
| ); | ||
| console.log('🔑 하드코딩된 토큰으로 설정했습니다.'); | ||
| return; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
토큰 설정 후 즉시 return으로 데이터 로드가 영구히 스킵됨(초기 로딩 스피너 고착).
현재 분기에서는 토큰을 저장만 하고 바로 return하여 setTabLoading/데이터 로드가 실행되지 않습니다. 의도치 않게 초기 로딩 상태가 풀리지 않을 수 있습니다. DEV 임시 토큰을 쓰더라도 로딩은 이어가 주세요.
- if (!authToken) {
- localStorage.setItem(
- 'authToken',
- 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjksImlhdCI6MTc1Njc4MjEyNywiZXhwIjoxNzU5Mzc0MTI3fQ.iRU7rN90Vs9Wykxvw-gkyAkbyB-HQENm_WifYHb2UR8',
- );
- console.log('🔑 하드코딩된 토큰으로 설정했습니다.');
- return;
- }
+ if (!authToken) {
+ if (import.meta.env.DEV && import.meta.env.VITE_DEV_AUTH_TOKEN) {
+ localStorage.setItem('authToken', import.meta.env.VITE_DEV_AUTH_TOKEN);
+ console.log('🔑 DEV 토큰을 설정했습니다.');
+ } else {
+ // 운영에서는 로그인 페이지로 보내거나 토큰 발급 플로우로 유도
+ navigate('/');
+ return;
+ }
+ }📝 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.
| localStorage.setItem( | |
| 'authToken', | |
| 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjksImlhdCI6MTc1Njc4MjEyNywiZXhwIjoxNzU5Mzc0MTI3fQ.iRU7rN90Vs9Wykxvw-gkyAkbyB-HQENm_WifYHb2UR8', | |
| ); | |
| console.log('🔑 하드코딩된 토큰으로 설정했습니다.'); | |
| return; | |
| } | |
| if (!authToken) { | |
| if (import.meta.env.DEV && import.meta.env.VITE_DEV_AUTH_TOKEN) { | |
| localStorage.setItem('authToken', import.meta.env.VITE_DEV_AUTH_TOKEN); | |
| console.log('🔑 DEV 토큰을 설정했습니다.'); | |
| } else { | |
| // 운영에서는 로그인 페이지로 보내거나 토큰 발급 플로우로 유도 | |
| navigate('/'); | |
| return; | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/pages/feed/Feed.tsx around lines 163 to 169, the branch that writes a
hardcoded dev token to localStorage immediately returns, which prevents the
subsequent data-loading and setTabLoading flow and causes the initial loading
spinner to hang; remove the early return so execution continues after setting
the token (or explicitly call the same data-load and setTabLoading logic used in
the normal path), ensuring the component proceeds to fetch data and clear the
loading state even when using the DEV token.
#️⃣ 연관된 이슈
#231
📝작업 내용
1. API 함수 생성:
src/api/rooms/deleteDailyGreeting.tsDELETE /rooms/{roomId}/daily-greeting/{attendanceCheckId}엔드포인트 연동2. MessageList 컴포넌트 수정:
roomIdprops 추가3. TodayWords 페이지 수정:
roomId와onMessageDelteprops 전달Summary by CodeRabbit