Skip to content
Open
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
61 changes: 47 additions & 14 deletions src/components/studyReport/RecordFileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,28 @@ import {
X,
Paperclip,
} from 'lucide-react'
import 'react-toastify/dist/ReactToastify.css'
import { toast } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'

interface RecordFileUploadProps {
files: File[]
existingFiles?: { id: number; name: string }[]
onFilesChange: (files: File[]) => void
onExistingFilesChange?: (files: { id: number; name: string }[]) => void
}

const MAX_TOTAL_SIZE = 10 * 1024 * 1024 // 10MB

export const RecordFileUpload = ({
files,
existingFiles = [],
onFilesChange,
onExistingFilesChange,
}: RecordFileUploadProps) => {
const [dragActive, setDragActive] = useState(false)
const inputRef = useRef<HTMLInputElement | null>(null)

// λͺ¨λ°”일 ν™˜κ²½ 감지
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)

// 파일 선택 (클릭/νƒ­)
const handleFileSelect = (e: ChangeEvent<HTMLInputElement>) => {
const selected = e.target.files ? Array.from(e.target.files) : []
const totalSize =
Expand All @@ -42,7 +43,6 @@ export const RecordFileUpload = ({
toast.success(`${selected.length}개의 파일이 μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€.`)
}

// λ“œλž˜κ·Έ μ•€ λ“œλ‘­ (λ°μŠ€ν¬ν†± μ „μš©)
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
Expand All @@ -64,9 +64,14 @@ export const RecordFileUpload = ({
toast.info(`파일 "${name}"이 μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.`)
}

// 파일 μ•„μ΄μ½˜ κ²°μ •
const getFileIcon = (file: File) => {
const type = file.type
const handleRemoveExistingFile = (id: number, name: string) => {
if (!onExistingFilesChange) return
onExistingFilesChange(existingFiles.filter((f) => f.id !== id))
toast.info(`파일 "${name}"이 μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.`)
}

const getFileIcon = (file: File | { name: string }) => {
const type = 'type' in file ? file.type : ''
const ext = file.name.split('.').pop()?.toLowerCase()

if (type.startsWith('image/'))
Expand Down Expand Up @@ -100,7 +105,6 @@ export const RecordFileUpload = ({
<div>
<label className="mb-2 block font-semibold">첨뢀 파일</label>

{/* μ—…λ‘œλ“œ μ˜μ—­ */}
<div
className={`cursor-pointer rounded-xl border-2 border-dashed py-10 text-center transition-colors ${
dragActive
Expand Down Expand Up @@ -148,20 +152,18 @@ export const RecordFileUpload = ({
type="file"
multiple
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.csv,.json,.html"
capture="environment"
className="hidden"
onChange={handleFileSelect}
/>
</div>

{/* 파일 리슀트 */}
{/* μƒˆ 파일 리슀트 */}
{files.length > 0 && (
<div className="mt-5 rounded-lg border border-gray-200 bg-gray-50 p-4">
<div className="mb-3 flex items-center gap-2 font-medium">
<Paperclip className="text-gray-700" size={16} />
첨뢀 파일 ({files.length}개)
<Paperclip className="text-gray-700" size={16} />μƒˆ 첨뢀 파일 (
{files.length}개)
</div>

<div className="grid gap-2 sm:grid-cols-2">
{files.map((file) => (
<div
Expand All @@ -185,6 +187,37 @@ export const RecordFileUpload = ({
</div>
</div>
)}

{/* 기쑴 파일 리슀트 */}
{existingFiles.length > 0 && (
<div className="mt-3 rounded-lg border border-gray-200 bg-gray-50 p-4">
<div className="mb-3 flex items-center gap-2 font-medium">
<Paperclip className="text-gray-700" size={16} />
κΈ°μ‘΄ 첨뢀 파일 ({existingFiles.length}개)
</div>
<div className="grid gap-2 sm:grid-cols-2">
{existingFiles.map((file) => (
<div
key={file.id}
className="flex items-center justify-between rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm hover:shadow"
>
<div className="flex min-w-0 items-center gap-2 text-sm text-gray-700">
{getFileIcon(file)}
<span className="max-w-[180px] truncate overflow-hidden whitespace-nowrap">
{file.name}
</span>
</div>
<button
onClick={() => handleRemoveExistingFile(file.id, file.name)}
className="cursor-pointer text-gray-400 hover:text-gray-600"
>
<X size={16} />
</button>
</div>
))}
</div>
</div>
)}
</div>
)
}
6 changes: 3 additions & 3 deletions src/hooks/api/mutations/useStudyRecordMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { api } from '@/api/api'

interface CreateStudyRecordParams {
startDate?: string
endDate?: string
endDate?: string
}

export const useStudyRecordMutation = (groupId: string) => {
const createRecordMutation = useMutation({
mutationFn: (params: CreateStudyRecordParams) =>
api.v1.studies.groups(groupId).schedules.GET(undefined,{params}),
api.v1.studies.groups(groupId).schedules.GET(undefined, { params }),
onSuccess: () => {
// μŠ€ν„°λ”” 기둝 λͺ©λ‘ μƒˆλ‘œκ³ μΉ¨
queryClient.invalidateQueries({
Expand All @@ -28,4 +28,4 @@ export const useStudyRecordMutation = (groupId: string) => {
return {
createRecordMutation,
}
}
}
110 changes: 79 additions & 31 deletions src/pages/study-groups/StudyRecord/StudyRecord.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,21 @@ import { RecordFileUpload } from '@/components/studyReport/RecordFileUpload'
import { RecordActionButtons } from '@/components/studyReport/RecordActionButtons'
import { RecordBreadcrumb } from '@/components/breadcrumb/RecordBreadcrumb'
import { toast } from 'react-toastify'

const MOCK_RECORD = {
title: 'μ˜ˆμ‹œ μŠ€ν„°λ”” 기둝 제λͺ©',
content: '여기에 ν•™μŠ΅ λ‚΄μš©μ„ μž‘μ„±ν•΄λ³΄μ„Έμš”.',
files: [],
}
import { api } from '@/api/api'

export const StudyRecord = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [files, setFiles] = useState<File[]>([])
const [mode, setMode] = useState<'create' | 'edit'>('create')
const [loading, setLoading] = useState(false)

const navigate = useNavigate()
const { studyGroupId, studyRecordId } = useParams<{
studyGroupId: string
studyRecordId: string
}>()

// λ“œλž˜κ·Έ μ•€ λ“œλ‘­ λ°©μ§€
useEffect(() => {
const preventDefault = (e: DragEvent) => {
e.preventDefault()
Expand All @@ -40,23 +36,35 @@ export const StudyRecord = () => {
}
}, [])

// 기둝 데이터 뢈러였기
const loadStudyRecord = async (groupId: string, recordId: string) => {
console.log(`(MOCK) 그룹 ${groupId} 기둝 ${recordId} 뢈러였기`)
await new Promise((resolve) => setTimeout(resolve, 300))
setTitle(MOCK_RECORD.title)
setContent(MOCK_RECORD.content)
setFiles(MOCK_RECORD.files)
/** 기둝 뢈러였기 */
const loadStudyRecord = async (recordId: string) => {
try {
setLoading(true)

const res = await api.v1.studies.notes(Number(recordId)).GET()
const data = res.data

if (!data) {
toast.error('기둝 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.')
return
}
setFiles([])
} catch {
toast.error('기둝 정보λ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.')
} finally {
setLoading(false)
}
}

/** 기둝 μˆ˜μ • λͺ¨λ“œμΌ 경우 데이터 뢈러였기 */
useEffect(() => {
if (studyGroupId && studyRecordId) {
if (studyRecordId) {
setMode('edit')
loadStudyRecord(studyGroupId, studyRecordId)
loadStudyRecord(studyRecordId)
} else if (window.location.pathname.includes('edit')) {
setMode('edit')
}
}, [studyGroupId, studyRecordId])
}, [studyRecordId])

const handleFilesChange = (newFiles: File[]) => setFiles(newFiles)

Expand All @@ -67,24 +75,58 @@ export const StudyRecord = () => {
toast.info('μž‘μ„± 쀑인 λ‚΄μš©μ΄ μ΄ˆκΈ°ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€.')
}

/** μ €μž₯ / μˆ˜μ • */
const handleSave = async () => {
// λ‚΄μš©μ΄ μ—†μœΌλ©΄ ν† μŠ€νŠΈλ‘œ μ•ˆλ‚΄ν•˜κ³  μ €μž₯ 막기
if (!studyGroupId) {
toast.error('잘λͺ»λœ μ ‘κ·Όμž…λ‹ˆλ‹€. κ·Έλ£Ή 정보가 μ—†μŠ΅λ‹ˆλ‹€.')
return
}

if (title.trim() === '' || content.trim() === '') {
toast.warn('제λͺ©κ³Ό λ‚΄μš©μ„ λͺ¨λ‘ μž…λ ₯ν•΄μ•Ό μ €μž₯ν•  수 μžˆμŠ΅λ‹ˆλ‹€.')
return
}

const recordData = { title, content, files }
console.log('(MOCK) μ €μž₯ 데이터:', recordData)
await new Promise((resolve) => setTimeout(resolve, 300))

if (mode === 'edit') {
toast.success('μŠ€ν„°λ”” 기둝이 μ„±κ³΅μ μœΌλ‘œ μˆ˜μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€.')
} else {
toast.success('μƒˆ μŠ€ν„°λ”” 기둝이 μ„±κ³΅μ μœΌλ‘œ μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€.')
try {
setLoading(true)

const attachments: [] = [] // 파일 μ—…λ‘œλ“œ μ‹œ μ—¬κΈ°μ„œ λŒ€μ²΄λ¨

if (mode === 'edit' && studyRecordId) {
const res = await api.v1.studies.notes(Number(studyRecordId)).PATCH({
title,
content_md: content,
attachments,
})

if (!res.data) {
toast.error('기둝을 μˆ˜μ •ν•  수 μ—†μŠ΅λ‹ˆλ‹€.')
return
}

toast.success('μŠ€ν„°λ”” 기둝이 μ„±κ³΅μ μœΌλ‘œ μˆ˜μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€.')
} else {
const res = await api.v1.studies.notes.POST({
group_id: studyGroupId,
title,
content_md: content,
attachments,
})

if (!res.data) {
toast.error('μƒˆ 기둝을 μ €μž₯ν•  수 μ—†μŠ΅λ‹ˆλ‹€.')
return
}

toast.success('μƒˆ μŠ€ν„°λ”” 기둝이 μ„±κ³΅μ μœΌλ‘œ μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€.')
}

navigate(`/study_group_detail/${studyGroupId}`)
} catch {
toast.error('μ €μž₯ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.')
} finally {
setLoading(false)
}

if (studyGroupId) navigate(`/study_group_detail/${studyGroupId}`)
}

return (
Expand All @@ -102,9 +144,15 @@ export const StudyRecord = () => {
</div>

<div className="w-full max-w-3xl rounded-2xl border border-gray-200 bg-white p-[25px]">
<RecordTitleInput title={title} setTitle={setTitle} />
<RecordMarkdownEditor content={content} setContent={setContent} />
<RecordFileUpload files={files} onFilesChange={handleFilesChange} />
{loading ? (
<p className="text-center text-gray-400">λΆˆλŸ¬μ˜€λŠ” 쀑...</p>
) : (
<>
<RecordTitleInput title={title} setTitle={setTitle} />
<RecordMarkdownEditor content={content} setContent={setContent} />
<RecordFileUpload files={files} onFilesChange={handleFilesChange} />
</>
)}
</div>

<div className="mt-6 flex w-full max-w-3xl justify-between">
Expand Down
Loading