diff --git a/package-lock.json b/package-lock.json index 900cb78..80e7396 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@mui/icons-material": "^7.1.0", @@ -348,6 +351,73 @@ "node": ">=6.9.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/modifiers": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", + "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -4774,6 +4844,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index e435a0f..532dd90 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,9 @@ "preview": "vite preview" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@mui/icons-material": "^7.1.0", diff --git a/src/lib/api.ts b/src/lib/api.ts index 9c788fe..7f420d7 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,8 +1,8 @@ import axios from 'axios' const api = axios.create({ - baseURL: 'https://api.biryu2000.kr', - // baseURL: 'http://localhost:8000', + // baseURL: 'https://api.biryu2000.kr', + baseURL: 'http://localhost:8000', }) // 요청 시 토큰 자동 추가 diff --git a/src/pages/CharacterList.tsx b/src/pages/CharacterList.tsx index 60a2d38..94a0908 100644 --- a/src/pages/CharacterList.tsx +++ b/src/pages/CharacterList.tsx @@ -1,8 +1,25 @@ import { Box, Card, CardContent, Typography, Container, Grid } from '@mui/material' import { useEffect, useState } from 'react' -import { useNavigate, Link } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import api from '../lib/api' +import { + DndContext, + closestCenter, + DragEndEvent, + PointerSensor, + useSensor, + useSensors, +} from '@dnd-kit/core' +import { + arrayMove, + SortableContext, + useSortable, + verticalListSortingStrategy, +} from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import DragIndicatorIcon from '@mui/icons-material/DragIndicator' + interface Character { id: number name: string @@ -11,61 +28,122 @@ interface Character { combat_power?: number } +function SortableCharacterCard({ character }: { character: Character }) { + const navigate = useNavigate() + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: character.id }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + zIndex: isDragging ? 1000 : 'auto', + position: 'relative' as const, + } + + return ( + + navigate(`/characters/${character.id}/edit`)} + > + {/* 드래그 핸들 */} + e.stopPropagation()} + > + + + + + {character.name} + 서버: {character.server || '-'} + 직업: {character.job || '-'} + + 전투력: {character.combat_power?.toLocaleString() || '-'} + + + + + ) +} + export default function CharacterList() { const [characters, setCharacters] = useState([]) useEffect(() => { - const fetchCharacters = async () => { - try { - const response = await api.get('/characters') - setCharacters(response.data) - } catch (err) { - console.error('캐릭터 목록을 불러오는 중 오류 발생:', err) - } - } - - fetchCharacters() + api.get('/characters').then((res) => setCharacters(res.data)) }, []) + const sensors = useSensors(useSensor(PointerSensor)) + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event + if (!over || active.id === over.id) return + + const oldIndex = characters.findIndex((c) => c.id === active.id) + const newIndex = characters.findIndex((c) => c.id === over.id) + + const newCharacters = arrayMove(characters, oldIndex, newIndex) + setCharacters(newCharacters) + + const payload = newCharacters.map((c, index) => ({ + id: c.id, + order: index + 1, + })) + api.patch('/characters/order', payload) + } + return ( 캐릭터 목록 - - {characters.map((char) => ( - - - - {char.name} - 서버: {char.server || '-'} - 직업: {char.job || '-'} - 전투력: {char.combat_power?.toLocaleString() || '-'} - - + + c.id)} strategy={verticalListSortingStrategy}> + + {characters.map((char) => ( + + ))} + + (window.location.href = '/characters/register')} + sx={{ + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + textDecoration: 'none', + cursor: 'pointer', + }} + > + + + + 캐릭터 추가 + + + + - ))} - - - - - + 캐릭터 추가 - - - - - + + ) } diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 6ef2172..5734daa 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,37 +1,71 @@ -import { Box, Typography, Button } from '@mui/material' +import { Box, Typography, Button, Divider } from '@mui/material' import { useAuth } from '../contexts/AuthContext' import { Link } from 'react-router-dom' export default function Home() { const { isLoggedIn } = useAuth() - if (!isLoggedIn) { - return ( - - 숙제노기에 오신 걸 환영합니다 📝 - - 이곳은 당신의 마비노기 숙제를 손쉽게 관리하는 웹 앱입니다. - - - - ) - } + const updates = [ + { date: '2025-05-28', version: 'v1.1', content: '캐릭터 및 숙제 목록에서 순서변경기능 추가' }, + { date: '2025-05-28', version: 'v1.1', content: '홈화면에 업데이트 내역 노출' }, + ] - // ✅ 로그인 상태일 때 "컨텐츠 들어갈 자리"만 출력 return ( - 길드 '노인정'과 함께합니다. 누구마음대로? + {!isLoggedIn ? ( + <> + + + 숙제노기에 오신 걸 환영합니다 📝 + + + 이곳은 당신의 마비노기 숙제를 손쉽게 관리하는 웹 앱입니다. + + + - {/* ✅ 이미지 추가 */} - - 노인정길드 - + + 📌 업데이트 내역 + {updates.map((u, idx) => ( + + + {u.date} | {u.version} + + - {u.content} + + ))} + + + + ) : ( + <> + 📌 업데이트 내역 + + {updates.map((u, idx) => ( + + + {u.date} | {u.version} + + - {u.content} + + ))} + + + + + 길드 '노인정'과 함께합니다. 누구마음대로? + + + 노인정길드 + + + )} ) } diff --git a/src/pages/HomeworkList.tsx b/src/pages/HomeworkList.tsx index 3891940..2a5536b 100644 --- a/src/pages/HomeworkList.tsx +++ b/src/pages/HomeworkList.tsx @@ -1,9 +1,26 @@ import { Box, Card, CardContent, Typography } from '@mui/material' import Grid from '@mui/material/Grid' -import { Link } from 'react-router-dom' import { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' import api from '../lib/api' +import { + DndContext, + closestCenter, + DragEndEvent, + PointerSensor, + useSensor, + useSensors, +} from '@dnd-kit/core' +import { + arrayMove, + SortableContext, + useSortable, + verticalListSortingStrategy, +} from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import DragIndicatorIcon from '@mui/icons-material/DragIndicator' + interface HomeworkType { id: number title: string @@ -12,63 +29,119 @@ interface HomeworkType { clear_count: number } +function SortableHomeworkCard({ homework }: { homework: HomeworkType }) { + const navigate = useNavigate() + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id: homework.id, + }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + zIndex: isDragging ? 1000 : 'auto', + position: 'relative' as const, + } + + return ( + + navigate(`/homeworks/${homework.id}/edit`)} + > + e.stopPropagation()} + > + + + + + {homework.title} + 주기: {homework.reset_type} + 횟수: {homework.clear_count} + + + + ) +} + export default function HomeworkList() { const [homeworks, setHomeworks] = useState([]) useEffect(() => { - const fetchHomeworks = async () => { - try { - const res = await api.get('/homeworks') - setHomeworks(res.data) - } catch (err) { - console.error('숙제 목록을 불러오는 데 실패했습니다.', err) - } - } - fetchHomeworks() + api.get('/homeworks').then((res) => setHomeworks(res.data)) }, []) + const sensors = useSensors(useSensor(PointerSensor)) + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event + if (!over || active.id === over.id) return + + const oldIndex = homeworks.findIndex((h) => h.id === active.id) + const newIndex = homeworks.findIndex((h) => h.id === over.id) + const newOrder = arrayMove(homeworks, oldIndex, newIndex) + setHomeworks(newOrder) + + const payload = newOrder.map((hw, idx) => ({ + id: hw.id, + order: idx + 1, + })) + api.patch('/homeworks/order', payload) + } + return ( 숙제 목록 - - - {homeworks.map((hw) => ( - - - - {hw.title} - 주기: {hw.reset_type} - 횟수: {hw.clear_count} - - + + h.id)} strategy={verticalListSortingStrategy}> + + {homeworks.map((hw) => ( + + ))} + + (window.location.href = '/homeworks/register')} + sx={{ + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + textDecoration: 'none', + cursor: 'pointer', + }} + > + + + + 숙제 추가 + + + + - ))} - - - - - + 숙제 추가 - - - - - + + ) }