ITN Dev 07-22
feat: 채팅 입력창 파일 드래그앤드롭 첨부 지원
@3ea6cc34a5bc33d467f9cce67d2735b62bef9d6f
frontend/src/components/ChannelPosts.test.tsx
--- frontend/src/components/ChannelPosts.test.tsx
+++ frontend/src/components/ChannelPosts.test.tsx
@@ -212,6 +212,24 @@
     expect(screen.queryByText('계약서.pdf')).toBeNull()
   })
 
+  it('입력창에 파일을 드래그앤드롭하면 첨부된다', async () => {
+    mocks.getPosts.mockResolvedValue([])
+
+    render(<ChannelPosts org={org()} />)
+
+    const dropzone = screen.getByTestId('composer-dropzone')
+    const file = new File(['x'], '드랍파일.pdf', { type: 'application/pdf' })
+
+    fireEvent.dragEnter(dropzone, { dataTransfer: { files: [file] } })
+    expect(screen.getByText('파일을 여기에 놓으면 첨부됩니다')).toBeTruthy()
+
+    fireEvent.drop(dropzone, { dataTransfer: { files: [file] } })
+
+    expect(screen.getByText('드랍파일.pdf')).toBeTruthy()
+    expect(screen.queryByText('파일을 여기에 놓으면 첨부됩니다')).toBeNull()
+    expect(screen.getByRole('button', { name: '보내기' })).toBeEnabled()
+  })
+
   it('공지가 있으면 배너로 보이고 확인 후 해제할 수 있다', async () => {
     mocks.getPosts.mockResolvedValue([])
     mocks.getNotice.mockResolvedValue(post({ id: 'n1', message: '중요 공지입니다' }))
frontend/src/components/ChannelPosts.tsx
--- frontend/src/components/ChannelPosts.tsx
+++ frontend/src/components/ChannelPosts.tsx
@@ -179,10 +179,14 @@
   const [sending, setSending] = useState(false)
   const [sendError, setSendError] = useState<string | null>(null)
   const [noticeWarning, setNoticeWarning] = useState<string | null>(null)
+  const [dragging, setDragging] = useState(false)
 
   const containerRef = useRef<HTMLDivElement>(null)
   const fileInputRef = useRef<HTMLInputElement>(null)
   const prevCountRef = useRef(0)
+  // dragenter/dragleave는 자식 요소를 지날 때마다 쌍으로 발생하므로,
+  // 카운터로 세지 않으면 안내 표시가 깜빡인다
+  const dragDepthRef = useRef(0)
 
   useEffect(() => {
     if (bothMissing) {
@@ -331,12 +335,43 @@
             )}
           </div>
 
-          {/* 입력창 */}
+          {/* 입력창 — 파일을 끌어다 놓으면 첨부된다 */}
           <div
-            className={`mt-3 rounded-lg border bg-white p-3 ${
-              asNotice ? 'border-amber-300' : 'border-gray-200'
+            data-testid="composer-dropzone"
+            onDragEnter={(e) => {
+              e.preventDefault()
+              dragDepthRef.current += 1
+              setDragging(true)
+            }}
+            onDragOver={(e) => e.preventDefault()}
+            onDragLeave={() => {
+              dragDepthRef.current = Math.max(0, dragDepthRef.current - 1)
+              if (dragDepthRef.current === 0) {
+                setDragging(false)
+              }
+            }}
+            onDrop={(e) => {
+              e.preventDefault()
+              dragDepthRef.current = 0
+              setDragging(false)
+              const dropped = Array.from(e.dataTransfer?.files ?? [])
+              if (dropped.length > 0) {
+                setAttachments((prev) => [...prev, ...dropped])
+              }
+            }}
+            className={`mt-3 rounded-lg border bg-white p-3 transition-colors ${
+              dragging
+                ? 'border-2 border-dashed border-blue-400 bg-blue-50/50'
+                : asNotice
+                  ? 'border-amber-300'
+                  : 'border-gray-200'
             }`}
           >
+            {dragging && (
+              <p className="mb-2 text-center text-xs font-medium text-blue-600">
+                파일을 여기에 놓으면 첨부됩니다
+              </p>
+            )}
             {attachments.length > 0 && (
               <div className="mb-2 flex flex-wrap gap-1.5">
                 {attachments.map((file, i) => (
Add a comment
List