Fix(#157): 드래그앤드롭 추가 및 노트 leftpanel ux/ui 수정#166
Conversation
📝 WalkthroughWalkthroughViewer 및 Storage 좌측 패널에 파일 업로드(선택·드래그앤드롭) 기능이 추가되었고, 이를 지원하는 훅(useFileUpload, useViewerFile)과 뷰어컴포넌트(ViewerEmpty, ViewerUploadCard, NoteCard)들이 연동되어 업로드, 검증(형식·플랜별 크기), 업로드 요청 및 쿼리 리프레시 흐름을 구현했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as ViewerContent / StorageContent
participant Hook as useFileUpload
participant Server as uploadAsset API
participant Q as QueryClient
User->>UI: 파일 드래그 또는 파일 선택
UI->>Hook: openFileExplorer / 드롭 / handleFileChange 호출
Hook->>Hook: isDragging = true (드래그 진입)
Hook-->>UI: onFileSelect로 파일 전달
UI->>UI: 파일 형식 검사 및 플랜별 크기 검사(parseSize, PLAN_DETAILS)
alt 검사 실패
UI-->>User: 오류 알림
else 검사 통과
UI->>Server: uploadAsset(file)
Server-->>UI: 업로드 응답(성공/실패)
alt 성공
UI->>Q: queryClient.refetchQueries(note detail, assets)
Q-->>UI: 최신 데이터
UI-->>User: 뷰어/스토리지 갱신
else 실패
UI-->>User: 업로드 실패 알림
end
end
Hook->>Hook: isDragging = false (드래그 종료)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@src/features/chat/components/leftpanel/StorageContent.tsx`:
- Around line 221-228: The code in StorageContent.tsx redundantly calls
queryClient.refetchQueries({ queryKey: noteKeys.detail(noteId) }) after
uploadAsset, but uploadAsset (see useAssetUpload.uploadAsset) already refetches
noteKeys.detail(String(noteId)) and assetKeys.storage; remove the duplicate
refetch in StorageContent.tsx (the await queryClient.refetchQueries(...) block)
so you don't trigger the same network request twice, or if you must refetch
additional keys only, call queryClient.refetchQueries for those specific keys
rather than noteKeys.detail.
In `@src/features/chat/components/leftpanel/ViewerContent.tsx`:
- Around line 24-31: The parseSize function incorrectly strips decimal points
causing "1.5GB" to parse as 15GB; update parseSize to extract the numeric
portion including the decimal (e.g. using a regex that captures ([\d.]+)) and
parse it with parseFloat, then extract the unit string (e.g. the captured
[A-Za-z]+) and normalize (toUpperCase) and apply the 1024-based multipliers to
the parsed float value; ensure this change is made in the parseSize function so
fractional sizes like 1.5GB are converted to bytes correctly.
- Around line 24-31: Extract the duplicated parseSize function into a single
shared utility module named parseSize.ts and export it; then replace the inline
definitions in ViewerContent and StorageContent by importing and using the
shared parseSize export. Ensure the extracted function preserves the existing
behavior (parses numeric value and unit, handles KB/MB/GB multipliers) and
update imports in both components to reference the new utility.
- Around line 71-97: The finally block unconditionally calls setIsLoading(false)
which conflicts with the component's useEffect that sets loading based on
activeFileId changes after a successful upload; modify the upload handler (the
async function that calls uploadAsset and uses setViewerFileId and
setSearchParams) to avoid clearing loading in finally on success — only call
setIsLoading(false) when the upload did not produce a valid result (no
result?.assetId) or when an error is caught; keep the success path reliant on
the useEffect that observes activeFileId to manage loading. Ensure to reference
the existing symbols: uploadAsset, setViewerFileId, setSearchParams,
setIsLoading, useEffect, and activeFileId when making the conditional change.
In `@src/features/chat/components/leftpanel/ViewerEmpty.tsx`:
- Around line 9-12: The clickable container in the ViewerEmpty component is a
<div> without accessibility attributes; update the element that currently uses
onOpenFileExplorer to be an actual <button> or, if you must keep a div, add
keyboard and ARIA support: give it role="button", tabIndex={0}, an appropriate
aria-label (or aria-labelledby), and handle Enter/Space in an onKeyDown that
calls onOpenFileExplorer; ensure the element retains its existing className and
onClick behavior so screen-reader and keyboard users can focus and activate it.
In `@src/pages/hooks/useViewerFile.ts`:
- Around line 36-47: When the useFileUpload callback receives a file and
isFileAllowed(file) returns false, the code currently silently ignores it;
update the callback in useViewerFile (the function passed to useFileUpload) to
notify the user (match StorageContent.tsx behavior) — e.g., call alert(...) or
the same feedback helper used in StorageContent.tsx when isFileAllowed(file) is
false before returning. Keep existing behavior when isFileAllowed(file) is true
(setFileName, viewerFileRef.current, setPdfUrl) and ensure you reference the
same validation path (isFileAllowed) so invalid files always produce
user-visible feedback.
🧹 Nitpick comments (9)
src/features/chat/components/leftpanel/StorageContent.tsx (4)
50-57:parseSize는 소수점 값을 처리하지 못합니다.
parseInt는 소수점 이하를 버리므로"1.5GB"같은 값이 입력되면1GB로 처리됩니다. 현재 플랜 설정이 정수값만 사용한다면 당장 문제는 없지만, 향후 변경 시 버그가 될 수 있습니다.제안: parseFloat 사용
const parseSize = (sizeStr: string) => { - const value = parseInt(sizeStr.replace(/\D/g, ""), 10); + const value = parseFloat(sizeStr.replace(/[^0-9.]/g, "")); const unit = sizeStr.replace(/[^A-Za-z]/g, "").toUpperCase(); if (unit.includes("GB")) return value * 1024 * 1024 * 1024; if (unit.includes("MB")) return value * 1024 * 1024; if (unit.includes("KB")) return value * 1024; return value; };
181-240:handleFileSelect가useCallback으로 감싸져 있지 않아 드래그 핸들러가 매 렌더마다 재생성됩니다.
handleFileSelect가 일반 함수로 정의되어useFileUpload에 전달되고 있습니다.useFileUpload내부의handleFileChange,onDrop등이[onFileSelect]를 의존성으로 갖고 있어,StorageContent가 리렌더될 때마다 모든 드래그 핸들러가 새로 생성됩니다. 이로 인해 드래그 중 상태가 예기치 않게 리셋될 수 있습니다.제안: useCallback 적용
- const handleFileSelect = async (file: File) => { + const handleFileSelect = useCallback(async (file: File) => { // ... 기존 로직 동일 - }; + }, [user, noteId, uploadAsset, queryClient]);
useCallbackimport에 추가 필요:- import { useState, useMemo, useEffect } from "react"; + import { useState, useMemo, useEffect, useCallback } from "react";
389-398: 빈 프래그먼트<></>불필요.
boxFiles가 비어있을 때<></>를 렌더링하는 것은.map()이 빈 배열에서 아무것도 반환하지 않는 것과 동일합니다. 삼항 연산자 대신 단순히boxFiles.map(...)만 사용하면 됩니다.제안: 간소화
- {boxFiles.length > 0 ? ( - boxFiles.map((file) => ( + {boxFiles.map((file) => ( <NoteCard key={file.id} ... /> - )) - ) : ( - /* ... 긴 주석 ... */ - <></> - )} + ))}
184-190:handleFileSelect에서isFileAllowed함수로 검증 로직을 통일하세요.현재 인라인 검증
file.type.startsWith("image/")는 모든 이미지 형식을 허용하지만,isFileAllowed는ALLOWED_MIME_TYPES에 정의된 특정 형식(png, jpeg, webp)만 검증합니다. 추가로isFileAllowed는 확장자 폴백 처리와 파일 크기 검사도 포함하고 있어 더 견고합니다. 파일 탐색기와 드래그 앤 드롭 간의 불일치를 방지하려면isFileAllowed를 사용하여 검증을 통일하는 것을 권장합니다.src/shared/hooks/useFileUpload.tsx (1)
1-96: 드래그 앤 드롭 구현이 잘 되어 있습니다.
dragCounterref를 사용한 중첩 드래그 이벤트 처리 패턴이 올바르게 적용되어 있고, 모든 핸들러가useCallback으로 적절히 메모이제이션되어 있습니다.참고: 이 파일은 JSX를 포함하지 않으므로
.tsx대신.ts확장자가 더 적합합니다. As per coding guidelines, "Name hooks in camelCase withuseprefix (e.g.,useAuth.ts,useMathKeyboard.ts)"src/pages/components/ViewerUploadCard.tsx (1)
46-51:DragDropOverlay래핑 방식이ViewerContent.tsx와 일관성이 없습니다.
ViewerContent.tsx(Line 214)에서는DragDropOverlay를 래퍼 없이 직접 렌더링하는 반면, 여기서는z-50래퍼div로 감싸고 있습니다.DragDropOverlay컴포넌트 자체에 이미absolute inset-0 z-40이 포함되어 있으므로, 래퍼의z-50과 내부의z-40이 혼재되어 z-index 관리가 혼란스러워질 수 있습니다.
rounded-[12px]클리핑이 필요한 것이라면,DragDropOverlay자체에rounded-xl이 이미 적용되어 있으므로overflow-hidden래퍼만 유지하거나, 두 파일에서 동일한 패턴을 사용하는 것이 좋겠습니다.src/features/chat/components/leftpanel/ViewerContent.tsx (3)
24-31:parseSize헬퍼 함수 위치가 코딩 가이드라인과 다릅니다.가이드라인에 따르면 메인 컴포넌트를 파일 상단에 배치하고, 헬퍼 함수는 메인 컴포넌트 아래 또는 별도 파일에 정의해야 합니다.
parseSize는 컴포넌트 위에 정의되어 있습니다. 별도 유틸리티 파일(예:fileValidation.ts)로 이동하면 재사용성도 높일 수 있습니다.As per coding guidelines, "Place the main component at the top of the file; define helper functions or sub-components below the main component or in separate files".
176-188: 에러 재시도 시window.location.reload()대신 데이터 재요청을 고려해 주세요.페이지 전체 새로고침은 모든 상태를 잃게 되므로,
activeFileId를 트리거하여useEffect만 다시 실행하는 방식이 더 가벼운 재시도 방법입니다.♻️ 리트라이 로직 개선 예시
retryCount같은 상태를useEffect의존성에 추가하여, 버튼 클릭 시 해당 상태를 증가시키는 방식으로 재요청을 유도할 수 있습니다.+ const [retryCount, setRetryCount] = useState(0); // ... - }, [activeFileId]); + }, [activeFileId, retryCount]); // 에러 UI에서: - onClick={() => window.location.reload()} + onClick={() => setRetryCount((c) => c + 1)}
49-105:handleFileSelect를useCallback으로 감싸기.현재
handleFileSelect는 매 렌더링마다 새로운 함수가 생성되며, 이는useFileUpload의handleFileChange와onDrop콜백을 불필요하게 재생성하게 합니다.useCallback으로 감싸고 의존성 배열에noteId,user,uploadAsset등을 추가하면, 필요한 경우에만 함수가 업데이트되고useFileUpload의 내부 콜백들도 안정적으로 유지됩니다.const handleFileSelect = useCallback(async (file: File) => { // ... 기존 로직 }, [noteId, user, uploadAsset, setViewerFileId, setSearchParams, setIsLoading, setError]);
| const result = await uploadAsset(parsed, file); | ||
|
|
||
| if (result?.assetId) { | ||
| // 업로드 성공 시 쿼리 갱신 | ||
| await queryClient.refetchQueries({ queryKey: noteKeys.detail(noteId) }); | ||
| // 선택적으로 뷰어로 바로 이동할 수도 있지만, 요구사항은 "서버로 파일 업로드" 임. | ||
| // 여기서는 업로드 후 목록에 표시되는 것이 우선. | ||
| } |
There was a problem hiding this comment.
noteKeys.detail 쿼리가 중복으로 refetch됩니다.
useAssetUpload의 uploadAsset 내부에서 이미 noteKeys.detail(String(noteId))와 assetKeys.storage를 refetch하고 있습니다 (src/features/assets/hooks/useAssetUpload.ts Line 38-41 참조). Line 225에서 동일한 쿼리를 다시 refetch하므로 불필요한 네트워크 요청이 발생합니다.
제안: 중복 refetch 제거
const result = await uploadAsset(parsed, file);
- if (result?.assetId) {
- // 업로드 성공 시 쿼리 갱신
- await queryClient.refetchQueries({ queryKey: noteKeys.detail(noteId) });
- // 선택적으로 뷰어로 바로 이동할 수도 있지만, 요구사항은 "서버로 파일 업로드" 임.
- // 여기서는 업로드 후 목록에 표시되는 것이 우선.
- }
+ // uploadAsset 내부에서 noteKeys.detail 및 assetKeys.storage refetch를 이미 수행함📝 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 result = await uploadAsset(parsed, file); | |
| if (result?.assetId) { | |
| // 업로드 성공 시 쿼리 갱신 | |
| await queryClient.refetchQueries({ queryKey: noteKeys.detail(noteId) }); | |
| // 선택적으로 뷰어로 바로 이동할 수도 있지만, 요구사항은 "서버로 파일 업로드" 임. | |
| // 여기서는 업로드 후 목록에 표시되는 것이 우선. | |
| } | |
| const result = await uploadAsset(parsed, file); | |
| // uploadAsset 내부에서 noteKeys.detail 및 assetKeys.storage refetch를 이미 수행함 |
🤖 Prompt for AI Agents
In `@src/features/chat/components/leftpanel/StorageContent.tsx` around lines 221 -
228, The code in StorageContent.tsx redundantly calls
queryClient.refetchQueries({ queryKey: noteKeys.detail(noteId) }) after
uploadAsset, but uploadAsset (see useAssetUpload.uploadAsset) already refetches
noteKeys.detail(String(noteId)) and assetKeys.storage; remove the duplicate
refetch in StorageContent.tsx (the await queryClient.refetchQueries(...) block)
so you don't trigger the same network request twice, or if you must refetch
additional keys only, call queryClient.refetchQueries for those specific keys
rather than noteKeys.detail.
| const parseSize = (sizeStr: string) => { | ||
| const value = parseInt(sizeStr.replace(/\D/g, ""), 10); | ||
| const unit = sizeStr.replace(/[^A-Za-z]/g, "").toUpperCase(); | ||
| if (unit.includes("GB")) return value * 1024 * 1024 * 1024; | ||
| if (unit.includes("MB")) return value * 1024 * 1024; | ||
| if (unit.includes("KB")) return value * 1024; | ||
| return value; | ||
| }; |
There was a problem hiding this comment.
parseSize: 소수점이 포함된 크기 문자열 파싱 시 버그 가능성.
replace(/\D/g, "") 는 소수점(.)도 제거하므로, "1.5GB" 같은 입력이 15GB로 잘못 파싱됩니다. 현재 PLAN_DETAILS의 값이 정수 기반이면 당장 문제가 되지 않지만, 향후 소수점 값이 추가되면 잘못된 크기 제한이 적용됩니다.
🛠️ 소수점 지원을 위한 수정 제안
const parseSize = (sizeStr: string) => {
- const value = parseInt(sizeStr.replace(/\D/g, ""), 10);
+ const value = parseFloat(sizeStr.replace(/[^0-9.]/g, ""));
const unit = sizeStr.replace(/[^A-Za-z]/g, "").toUpperCase();
if (unit.includes("GB")) return value * 1024 * 1024 * 1024;
if (unit.includes("MB")) return value * 1024 * 1024;
if (unit.includes("KB")) return value * 1024;
return value;
};🤖 Prompt for AI Agents
In `@src/features/chat/components/leftpanel/ViewerContent.tsx` around lines 24 -
31, The parseSize function incorrectly strips decimal points causing "1.5GB" to
parse as 15GB; update parseSize to extract the numeric portion including the
decimal (e.g. using a regex that captures ([\d.]+)) and parse it with
parseFloat, then extract the unit string (e.g. the captured [A-Za-z]+) and
normalize (toUpperCase) and apply the 1024-based multipliers to the parsed float
value; ensure this change is made in the parseSize function so fractional sizes
like 1.5GB are converted to bytes correctly.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
rg -n "parseSize" --type=ts --type=tsxRepository: Team-Proovy/Proovy-front
Length of output: 95
🏁 Script executed:
rg -n "parseSize" --type-list | head -20Repository: Team-Proovy/Proovy-front
Length of output: 457
🏁 Script executed:
rg -n "parseSize" -e "parseSize"Repository: Team-Proovy/Proovy-front
Length of output: 117
🏁 Script executed:
rg "parseSize" -A 3 -B 1Repository: Team-Proovy/Proovy-front
Length of output: 2042
parseSize 유틸리티 함수를 공통 모듈로 추출하세요.
parseSize 함수가 ViewerContent.tsx와 StorageContent.tsx에 동일하게 중복 정의되어 있습니다. DRY 원칙을 준수하기 위해 공통 유틸리티 파일(예: src/features/chat/utils/parseSize.ts)로 추출하고 두 파일에서 임포트하여 사용하도록 리팩토링하세요.
🤖 Prompt for AI Agents
In `@src/features/chat/components/leftpanel/ViewerContent.tsx` around lines 24 -
31, Extract the duplicated parseSize function into a single shared utility
module named parseSize.ts and export it; then replace the inline definitions in
ViewerContent and StorageContent by importing and using the shared parseSize
export. Ensure the extracted function preserves the existing behavior (parses
numeric value and unit, handles KB/MB/GB multipliers) and update imports in both
components to reference the new utility.
| try { | ||
| setIsLoading(true); | ||
| setError(null); | ||
| const parsed = parseInt(noteId, 10); | ||
| if (!Number.isInteger(parsed) || parsed <= 0) { | ||
| setError("유효하지 않은 노트입니다. 노트를 다시 열어주세요."); | ||
| return; | ||
| } | ||
|
|
||
| const result = await uploadAsset(parsed, file); | ||
|
|
||
| if (result?.assetId) { | ||
| setViewerFileId(result.assetId); | ||
| setSearchParams((prev) => { | ||
| prev.set("panel", "viewer"); | ||
| prev.set("file", result.assetId.toString()); | ||
| return prev; | ||
| }); | ||
| // 업로드 성공 후 로딩 상태 해제는 activeFileId 변경에 의한 useEffect나 완료 시점에서 처리 | ||
| } | ||
| } catch (err) { | ||
| console.error("파일 업로드 실패:", err); | ||
| setError("파일 업로드에 실패했습니다."); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
업로드 성공 시 isLoading 상태 플리커 가능성.
finally 블록(Line 94-96)에서 setIsLoading(false)를 무조건 호출하는데, 업로드 성공 후 setViewerFileId(Line 83)로 인해 activeFileId가 변경되면 useEffect(Line 108)가 다시 setIsLoading(true)를 설정합니다. 이로 인해 로딩 상태가 true → false → true로 깜빡이는 현상이 발생할 수 있습니다.
Line 89의 주석 의도("업로드 성공 후 로딩 상태 해제는 activeFileId 변경에 의한 useEffect에서 처리")와 finally의 동작이 충돌합니다.
🛠️ 성공 시 finally에서 isLoading을 리셋하지 않도록 수정
+ let uploadSucceeded = false;
try {
setIsLoading(true);
setError(null);
const parsed = parseInt(noteId, 10);
if (!Number.isInteger(parsed) || parsed <= 0) {
setError("유효하지 않은 노트입니다. 노트를 다시 열어주세요.");
return;
}
const result = await uploadAsset(parsed, file);
if (result?.assetId) {
+ uploadSucceeded = true;
setViewerFileId(result.assetId);
setSearchParams((prev) => {
prev.set("panel", "viewer");
prev.set("file", result.assetId.toString());
return prev;
});
}
} catch (err) {
console.error("파일 업로드 실패:", err);
setError("파일 업로드에 실패했습니다.");
} finally {
- setIsLoading(false);
+ if (!uploadSucceeded) {
+ setIsLoading(false);
+ }
}📝 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.
| try { | |
| setIsLoading(true); | |
| setError(null); | |
| const parsed = parseInt(noteId, 10); | |
| if (!Number.isInteger(parsed) || parsed <= 0) { | |
| setError("유효하지 않은 노트입니다. 노트를 다시 열어주세요."); | |
| return; | |
| } | |
| const result = await uploadAsset(parsed, file); | |
| if (result?.assetId) { | |
| setViewerFileId(result.assetId); | |
| setSearchParams((prev) => { | |
| prev.set("panel", "viewer"); | |
| prev.set("file", result.assetId.toString()); | |
| return prev; | |
| }); | |
| // 업로드 성공 후 로딩 상태 해제는 activeFileId 변경에 의한 useEffect나 완료 시점에서 처리 | |
| } | |
| } catch (err) { | |
| console.error("파일 업로드 실패:", err); | |
| setError("파일 업로드에 실패했습니다."); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| let uploadSucceeded = false; | |
| try { | |
| setIsLoading(true); | |
| setError(null); | |
| const parsed = parseInt(noteId, 10); | |
| if (!Number.isInteger(parsed) || parsed <= 0) { | |
| setError("유효하지 않은 노트입니다. 노트를 다시 열어주세요."); | |
| return; | |
| } | |
| const result = await uploadAsset(parsed, file); | |
| if (result?.assetId) { | |
| uploadSucceeded = true; | |
| setViewerFileId(result.assetId); | |
| setSearchParams((prev) => { | |
| prev.set("panel", "viewer"); | |
| prev.set("file", result.assetId.toString()); | |
| return prev; | |
| }); | |
| // 업로드 성공 후 로딩 상태 해제는 activeFileId 변경에 의한 useEffect나 완료 시점에서 처리 | |
| } | |
| } catch (err) { | |
| console.error("파일 업로드 실패:", err); | |
| setError("파일 업로드에 실패했습니다."); | |
| } finally { | |
| if (!uploadSucceeded) { | |
| setIsLoading(false); | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@src/features/chat/components/leftpanel/ViewerContent.tsx` around lines 71 -
97, The finally block unconditionally calls setIsLoading(false) which conflicts
with the component's useEffect that sets loading based on activeFileId changes
after a successful upload; modify the upload handler (the async function that
calls uploadAsset and uses setViewerFileId and setSearchParams) to avoid
clearing loading in finally on success — only call setIsLoading(false) when the
upload did not produce a valid result (no result?.assetId) or when an error is
caught; keep the success path reliant on the useEffect that observes
activeFileId to manage loading. Ensure to reference the existing symbols:
uploadAsset, setViewerFileId, setSearchParams, setIsLoading, useEffect, and
activeFileId when making the conditional change.
| <div | ||
| onClick={onOpenFileExplorer} | ||
| className="flex h-full w-full cursor-pointer flex-col items-center justify-center gap-[16px] text-center transition-colors hover:text-[#2542F0]" | ||
| > |
There was a problem hiding this comment.
클릭 가능한 <div>에 접근성 속성이 누락되어 있습니다.
키보드 사용자가 이 영역에 포커스하거나 활성화할 수 없습니다. <button> 또는 접근성 속성을 추가해 주세요.
제안: 접근성 속성 추가
<div
onClick={onOpenFileExplorer}
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') onOpenFileExplorer(); }}
+ role="button"
+ tabIndex={0}
className="flex h-full w-full cursor-pointer flex-col items-center justify-center gap-[16px] text-center transition-colors hover:text-[`#2542F0`]"
>또는 <div> 대신 <button>으로 변경하는 것이 더 바람직합니다.
📝 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.
| <div | |
| onClick={onOpenFileExplorer} | |
| className="flex h-full w-full cursor-pointer flex-col items-center justify-center gap-[16px] text-center transition-colors hover:text-[#2542F0]" | |
| > | |
| <div | |
| onClick={onOpenFileExplorer} | |
| onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') onOpenFileExplorer(); }} | |
| role="button" | |
| tabIndex={0} | |
| className="flex h-full w-full cursor-pointer flex-col items-center justify-center gap-[16px] text-center transition-colors hover:text-[`#2542F0`]" | |
| > |
🤖 Prompt for AI Agents
In `@src/features/chat/components/leftpanel/ViewerEmpty.tsx` around lines 9 - 12,
The clickable container in the ViewerEmpty component is a <div> without
accessibility attributes; update the element that currently uses
onOpenFileExplorer to be an actual <button> or, if you must keep a div, add
keyboard and ARIA support: give it role="button", tabIndex={0}, an appropriate
aria-label (or aria-labelledby), and handle Enter/Space in an onKeyDown that
calls onOpenFileExplorer; ensure the element retains its existing className and
onClick behavior so screen-reader and keyboard users can focus and activate it.
| } = useFileUpload((file) => { | ||
| if (file && isFileAllowed(file)) { | ||
| setFileName(file.name); | ||
| viewerFileRef.current = file; | ||
| setPdfUrl(URL.createObjectURL(file)); | ||
| // 파일 크기나 형식 검증은 isFileAllowed에서 처리하지만, | ||
| // 추가적인 검증이 필요하면 여기서 처리. | ||
| // 하지만 isFileAllowed는 boolean만 반환. | ||
|
|
||
| // 파일 선택 시 note 생성을 바로 하진 않음. | ||
| } | ||
| }); |
There was a problem hiding this comment.
유효하지 않은 파일 선택 시 사용자 피드백 없음.
isFileAllowed(file)이 false를 반환하면 아무런 피드백 없이 파일이 무시됩니다. 특히 드래그 앤 드롭 시 사용자가 파일을 놓았는데 아무 반응이 없으면 혼란스러울 수 있습니다. StorageContent.tsx(Line 188)에서는 alert으로 안내하고 있으므로, 여기서도 동일하게 피드백을 제공하는 것이 좋습니다.
제안: 유효하지 않은 파일에 대한 피드백 추가
} = useFileUpload((file) => {
if (file && isFileAllowed(file)) {
setFileName(file.name);
viewerFileRef.current = file;
setPdfUrl(URL.createObjectURL(file));
- // 파일 크기나 형식 검증은 isFileAllowed에서 처리하지만,
- // 추가적인 검증이 필요하면 여기서 처리.
- // 하지만 isFileAllowed는 boolean만 반환.
-
- // 파일 선택 시 note 생성을 바로 하진 않음.
+ } else if (file) {
+ alert("지원하지 않는 파일 형식이거나 파일 크기가 초과되었습니다.");
}
});🤖 Prompt for AI Agents
In `@src/pages/hooks/useViewerFile.ts` around lines 36 - 47, When the
useFileUpload callback receives a file and isFileAllowed(file) returns false,
the code currently silently ignores it; update the callback in useViewerFile
(the function passed to useFileUpload) to notify the user (match
StorageContent.tsx behavior) — e.g., call alert(...) or the same feedback helper
used in StorageContent.tsx when isFileAllowed(file) is false before returning.
Keep existing behavior when isFileAllowed(file) is true (setFileName,
viewerFileRef.current, setPdfUrl) and ensure you reference the same validation
path (isFileAllowed) so invalid files always produce user-visible feedback.
stringnine
left a comment
There was a problem hiding this comment.
뷰어 업로드에서 드래그 앤 드롭 확인했씁니다
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/features/chat/components/leftpanel/StorageContent.tsx`:
- Line 82: The component StorageContent uses the hook useAuthStore but it isn't
imported; add the appropriate import for useAuthStore at the top of the file
(matching how it's exported from your auth store module) so the line "const {
user } = useAuthStore();" resolves correctly — update the import list in
StorageContent to import useAuthStore (respecting whether it's a named or
default export) and run type checks to confirm no further fixes are needed.
🧹 Nitpick comments (3)
src/features/chat/components/leftpanel/StorageContent.tsx (3)
83-84: 로컬isUploading상태와useAssetUpload의isUploading이 중복됩니다.
useAssetUpload훅이 이미isUploading상태를 제공하는데(relevant snippet 참조), Line 84에서 별도의 로컬isUploadingstate를 관리하고 있습니다. 업로드 중 예외가 발생하면 훅 내부의isUploading과 로컬 상태가 불일치할 수 있습니다.
useAssetUpload에서 반환하는isUploading을 직접 사용하면 상태 동기화 문제를 방지할 수 있습니다.♻️ 로컬 state 제거 및 훅 상태 활용 제안
- const { uploadAsset } = useAssetUpload(); - const [isUploading, setIsUploading] = useState(false); + const { uploadAsset, isUploading } = useAssetUpload();그리고
handleFileSelect내부의setIsUploading(true)/setIsUploading(false)호출을 모두 제거하세요.Also applies to: 219-219, 247-248
196-234:noteId유효성 검사를 파일 검증보다 먼저 수행하는 것이 좋습니다.현재 파일 타입/크기 검증 후에야
noteId존재 및 유효성을 확인합니다.noteId가 없으면 업로드 자체가 불가하므로, 불필요한 검증 로직을 실행하기 전에 먼저 확인하는 것이 사용자 경험과 로직 흐름에 더 자연스럽습니다.♻️ noteId 검증 순서 조정 제안
const handleFileSelect = async (file: File) => { if (!file) return; + const parsed = Number(noteId); + if (!noteId || isNaN(parsed) || parsed <= 0) { + alert("유효하지 않은 노트입니다. 노트를 다시 열어주세요."); + return; + } + const isValidType = file.type === "application/pdf" || file.type.startsWith("image/"); // ... 나머지 검증 ...
397-407: 빈 Fragment와 과도한 주석 블록을 정리해 주세요.
boxFiles가 비어있을 때 빈<></>를 반환하고 있으며, 6줄에 걸친 주석이 JSX 안에 남아 있습니다. 빈 배열에 대한.map()은 자연스럽게 아무것도 렌더링하지 않으므로, ternary 자체가 불필요합니다.♻️ 간소화 제안
- {boxFiles.length > 0 ? ( - boxFiles.map((file) => ( + {boxFiles.map((file) => ( <NoteCard ... /> - )) - ) : ( - /* 파일이 없을 때 메시지 ... */ - <></> - )} + ))}
gaeunee2
left a comment
There was a problem hiding this comment.
아주 조은 코드인걸 확인했습니다. 국잡.
📌 관련 이슈
🏷️ PR 타입
📝 작업 내용
📸 스크린샷
✅ 체크리스트
Summary by CodeRabbit
릴리스 노트
새로운 기능
품질 향상
스타일/사용성