diff --git a/src/app/[teamid]/_domain/apis/group.ts b/src/app/[teamid]/_domain/apis/group.ts deleted file mode 100644 index ee37940..0000000 --- a/src/app/[teamid]/_domain/apis/group.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { requestJson, requestVoid } from '@/shared/apis/groups/http'; -import type { Group, Task, UpdateGroupBody } from './types'; - -const GROUP_ERROR_MESSAGE = { - fetch: '그룹 정보 조회 실패', - update: '그룹 정보 수정 실패', - delete: '그룹 삭제 실패', - invitation: '초대 링크 조회 실패', - tasks: '그룹 작업 조회 실패', -} as const; - -export function getGroup(groupId: number): Promise { - return requestJson(`/groups/${groupId}`, GROUP_ERROR_MESSAGE.fetch); -} - -export function updateGroup(groupId: number, body: UpdateGroupBody): Promise { - return requestJson(`/groups/${groupId}`, GROUP_ERROR_MESSAGE.update, { - method: 'PATCH', - body: JSON.stringify(body), - }); -} - -export function deleteGroup(groupId: number): Promise { - return requestVoid(`/groups/${groupId}`, GROUP_ERROR_MESSAGE.delete, { - method: 'DELETE', - }); -} - -// API 응답: 초대 토큰 문자열 (JWT) 그대로 반환 -export function getGroupInvitation(groupId: number): Promise { - return requestJson(`/groups/${groupId}/invitation`, GROUP_ERROR_MESSAGE.invitation); -} - -export function getGroupTasks(groupId: number, date?: string): Promise { - const query = date ? `?date=${encodeURIComponent(date)}` : ''; - return requestJson(`/groups/${groupId}/tasks${query}`, GROUP_ERROR_MESSAGE.tasks); -} diff --git a/src/app/[teamid]/_domain/apis/task.ts b/src/app/[teamid]/_domain/apis/task.ts deleted file mode 100644 index e470b30..0000000 --- a/src/app/[teamid]/_domain/apis/task.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { requestJson } from '@/shared/apis/groups/http'; -import type { Task } from './types'; - -export function updateTask( - groupId: number, - taskListId: number, - taskId: number, - data: { done?: boolean; name?: string; description?: string }, -): Promise { - return requestJson( - `/groups/${groupId}/task-lists/${taskListId}/tasks/${taskId}`, - '할 일 수정 실패', - { - method: 'PATCH', - body: JSON.stringify(data), - }, - ); -} diff --git a/src/app/[teamid]/_domain/apis/taskList.ts b/src/app/[teamid]/_domain/apis/taskList.ts deleted file mode 100644 index d1149a6..0000000 --- a/src/app/[teamid]/_domain/apis/taskList.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { requestJson, requestVoid } from '@/shared/apis/groups/http'; -import type { TaskList } from './types'; - -const TASK_LIST_ERROR_MESSAGE = { - create: '작업 목록 생성 실패', - fetch: '작업 목록 조회 실패', - delete: '작업 목록 삭제 실패', -} as const; - -export function createTaskList(groupId: number, name: string): Promise { - return requestJson(`/groups/${groupId}/task-lists`, TASK_LIST_ERROR_MESSAGE.create, { - method: 'POST', - body: JSON.stringify({ name }), - }); -} - -export function getTaskList(groupId: number, taskListId: number, date?: string): Promise { - const query = date ? `?date=${encodeURIComponent(date)}` : ''; - return requestJson( - `/groups/${groupId}/task-lists/${taskListId}${query}`, - TASK_LIST_ERROR_MESSAGE.fetch, - ); -} - -export function deleteTaskList(groupId: number, taskListId: number): Promise { - return requestVoid( - `/groups/${groupId}/task-lists/${taskListId}`, - TASK_LIST_ERROR_MESSAGE.delete, - { method: 'DELETE' }, - ); -} diff --git a/src/app/[teamid]/_domain/apis/types.ts b/src/app/[teamid]/_domain/apis/types.ts deleted file mode 100644 index 454f016..0000000 --- a/src/app/[teamid]/_domain/apis/types.ts +++ /dev/null @@ -1,60 +0,0 @@ -export type MemberRole = 'ADMIN' | 'MEMBER'; - -export type FrequencyType = 'ONCE' | 'DAILY' | 'WEEKLY' | 'MONTHLY'; - -export interface GroupMember { - userId: number; - groupId: number; - userName: string; - userEmail: string; - userImage: string; - role: MemberRole; -} - -export interface TaskUser { - id: number; - nickname: string; - image: string | null; -} - -export interface Task { - id: number; - name: string; - description: string | null; - date: string; - doneAt: string | null; - updatedAt: string; - frequency: FrequencyType; - recurringId: number; - deletedAt: string | null; - commentCount: number; - displayIndex: number; - writer: TaskUser | null; - doneBy: { user: TaskUser | null } | null; -} - -export interface TaskList { - id: number; - name: string; - groupId: number; - displayIndex: number; - createdAt: string; - updatedAt: string; - tasks: Task[]; -} - -export interface Group { - id: number; - teamId: string; - name: string; - image: string; - createdAt: string; - updatedAt: string; - members: GroupMember[]; - taskLists: Omit[]; -} - -export interface UpdateGroupBody { - name?: string; - image?: string | null; -} diff --git a/src/app/[teamid]/_domain/components/Kanban/KanbanBoard.module.css b/src/app/[teamid]/_domain/components/Kanban/KanbanBoard.module.css deleted file mode 100644 index 41c3a20..0000000 --- a/src/app/[teamid]/_domain/components/Kanban/KanbanBoard.module.css +++ /dev/null @@ -1,85 +0,0 @@ -.wrapper { - display: flex; - flex-direction: column; - gap: 20px; -} - -.boardHeader { - display: flex; - align-items: center; - gap: 8px; -} - -.boardTitle { - font-size: 16px; - font-weight: 600; - color: var(--color-text-primary); - margin: 0; -} - -.boardCount { - color: var(--color-text-default); - font-weight: 400; -} - -.board { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 24px; - align-items: start; -} - -/* 태블릿: 컬럼을 한 줄씩 세로로 쌓기 */ -@media (min-width: 768px) and (max-width: 1279px) { - .board { - display: flex; - flex-direction: column; - gap: 16px; - } -} - -/* 모바일 */ -@media (max-width: 767px) { - .board { - display: flex; - flex-direction: column; - gap: 16px; - align-items: center; - max-width: 100%; - box-sizing: border-box; - overflow-x: hidden; - } -} - -.addListButton { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - max-width: 1140px; - padding: 14px 24px; - background: var(--color-background-primary); - border: 1px dashed var(--color-background-tertiary); - border-radius: 12px; - cursor: pointer; - font-size: 14px; - font-weight: 500; - color: var(--color-text-default); - font-family: inherit; - transition: - background 0.15s, - color 0.15s; -} - -.addListButton:hover { - background: var(--color-brand-secondary); - color: var(--color-brand-primary); - border-color: var(--color-brand-primary); -} - -.dragOverlay { - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); - border-radius: 12px; - opacity: 0.92; - cursor: grabbing; -} diff --git a/src/app/[teamid]/_domain/components/Kanban/KanbanBoard.tsx b/src/app/[teamid]/_domain/components/Kanban/KanbanBoard.tsx deleted file mode 100644 index 4f70511..0000000 --- a/src/app/[teamid]/_domain/components/Kanban/KanbanBoard.tsx +++ /dev/null @@ -1,98 +0,0 @@ -'use client'; - -import { DndContext, DragOverlay } from '@dnd-kit/core'; -import TodoCard from '@/components/todo-card/TodoCard'; -import AddTodoList from '@/components/Modal/domain/components/AddTodoList/AddTodoList'; -import KanbanColumn from './KanbanColumn'; -import styles from './KanbanBoard.module.css'; -import type { KanbanStatus } from '../../interfaces/team'; -import type { TaskList } from '../../apis/types'; -import { useKanbanTasks } from '../../hooks/useKanbanTasks'; -import { useKanbanDnd } from '../../hooks/useKanbanDnd'; - -export const KANBAN_COLUMNS: { id: KanbanStatus; label: string }[] = [ - { id: 'todo', label: '할 일' }, - { id: 'inProgress', label: '진행중' }, - { id: 'done', label: '완료' }, -]; - -const COLUMN_IDS = KANBAN_COLUMNS.map((c) => c.id); - -interface KanbanBoardProps { - groupId: number; - teamId: string; - taskLists: Omit[]; -} - -export default function KanbanBoard({ groupId, teamId, taskLists }: KanbanBoardProps) { - const { - tasks, - setTasks, - addingStatus, - newListTitle, - handleItemCheckedChange, - handleCardClick, - handleDeleteTask, - handleUpdateTask, - handleStatusChange, - handleAddTask, - handleAddListSubmit, - handleAddListClose, - handleNewListTitleChange, - } = useKanbanTasks(groupId, teamId, taskLists); - - const { activeTask, sensors, handleDragStart, handleDragEnd } = useKanbanDnd( - tasks, - setTasks, - COLUMN_IDS, - handleStatusChange, - ); - - return ( -
-
-

- 할 일 목록 ({tasks.length}개) -

-
- - -
- {KANBAN_COLUMNS.map((col) => ( - t.status === col.id)} - onItemCheckedChange={handleItemCheckedChange} - onCardClick={handleCardClick} - onAddTask={handleAddTask} - onDeleteTask={handleDeleteTask} - onUpdateTask={handleUpdateTask} - /> - ))} -
- - - {activeTask && ( -
- -
- )} -
-
- - void handleAddListSubmit()} - input={{ - props: { - value: newListTitle, - onChange: (e) => handleNewListTitleChange(e.target.value), - }, - }} - /> -
- ); -} diff --git a/src/app/[teamid]/_domain/components/Kanban/KanbanColumn.module.css b/src/app/[teamid]/_domain/components/Kanban/KanbanColumn.module.css deleted file mode 100644 index 34a14e8..0000000 --- a/src/app/[teamid]/_domain/components/Kanban/KanbanColumn.module.css +++ /dev/null @@ -1,55 +0,0 @@ -.column { - display: flex; - flex-direction: column; - gap: 12px; - width: 100%; -} - -.columnHeader { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 12px 0 24px; - background-color: var(--color-background-tertiary); - border-radius: 12px; - height: 38px; -} - -.columnTitle { - font-size: 16px; - font-weight: 600; - color: var(--color-text-primary); -} - -.addButton { - display: flex; - width: 24px; - height: 24px; - padding: 0; - border: none; - background: none; - cursor: pointer; - flex-shrink: 0; -} - -.itemList { - display: flex; - flex-direction: column; - gap: 16px; - min-height: 200px; - border-radius: 12px; - padding: 4px; - transition: background 0.15s; -} - -.isOver { - background: var(--color-brand-secondary); -} - -/* 드롭 가이드: 드래그 중인 아이템과 동일한 border-radius(12px)로 표시 */ -.dropGuide { - border-radius: 12px; - box-shadow: inset 0 0 0 2px var(--color-brand-primary); - background-color: var(--color-brand-secondary); - flex-shrink: 0; -} diff --git a/src/app/[teamid]/_domain/components/Kanban/KanbanColumn.tsx b/src/app/[teamid]/_domain/components/Kanban/KanbanColumn.tsx deleted file mode 100644 index 78ac523..0000000 --- a/src/app/[teamid]/_domain/components/Kanban/KanbanColumn.tsx +++ /dev/null @@ -1,88 +0,0 @@ -'use client'; - -import { memo } from 'react'; -import { useDroppable, useDndContext } from '@dnd-kit/core'; -import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; - -import KanbanItem from './KanbanItem'; -import styles from './KanbanColumn.module.css'; -import type { KanbanTask, KanbanStatus, TaskItem } from '../../interfaces/team'; -import Image from 'next/image'; -import Plus from '@/assets/buttons/plus/plusBoxButton.svg'; - -interface KanbanColumnProps { - status: KanbanStatus; - label: string; - tasks: KanbanTask[]; - onItemCheckedChange?: (taskId: string, itemId: string, checked: boolean) => void; - onCardClick?: (taskId: string) => void; - onAddTask?: (status: KanbanStatus) => void; - onDeleteTask?: (taskId: string) => void; - onEditTask?: (taskId: string) => void; - onUpdateTask?: (taskId: string, updatedData: { title: string; items: TaskItem[] }) => void; -} - -function KanbanColumn({ - status, - label, - tasks, - onItemCheckedChange, - onCardClick, - onAddTask, - onDeleteTask, - onEditTask, - onUpdateTask, -}: KanbanColumnProps) { - const { setNodeRef, isOver } = useDroppable({ id: status }); - const { active, over } = useDndContext(); - const itemIds = tasks.map((t) => t.id); - - // 현재 컬럼의 아이템이 드래그 중인지 확인 - const isActiveFromThisColumn = active ? tasks.some((t) => t.id === String(active.id)) : false; - - // 다른 컬럼 아이템을 이 컬럼의 기존 아이템 위로 드래그 중인지 확인 - const isOverThisColumnItem = over ? tasks.some((t) => t.id === String(over.id)) : false; - - // 다른 컬럼에서 이 컬럼으로 드래그 진입 시 드롭 가이드 표시 - const showDropGuide = !!active && !isActiveFromThisColumn && (isOver || isOverThisColumnItem); - - // 드래그 중인 아이템의 초기 높이 (가이드 크기를 아이템과 동일하게 맞추기 위함) - const draggedItemHeight = active?.rect.current.initial?.height ?? 54; - - return ( -
-
-

{label}

- -
- - -
- {tasks.map((task) => ( - - ))} - {showDropGuide && ( -
- )} -
- -
- ); -} - -export default memo(KanbanColumn); diff --git a/src/app/[teamid]/_domain/components/Kanban/KanbanItem.module.css b/src/app/[teamid]/_domain/components/Kanban/KanbanItem.module.css deleted file mode 100644 index be5e960..0000000 --- a/src/app/[teamid]/_domain/components/Kanban/KanbanItem.module.css +++ /dev/null @@ -1,156 +0,0 @@ -.item { - position: relative; - display: flex; - flex-direction: column; - cursor: pointer; - width: 100%; -} - -.cardWrapper { - position: relative; -} - -/* 드래그 중인 아이템: TodoCard와 동일한 border-radius(12px)로 테두리 플레이스홀더 표시 */ -.itemDragging { - border-radius: 12px; - box-shadow: inset 0 0 0 2px var(--color-brand-primary); - background-color: var(--color-brand-secondary); -} - -/* 드래그 중인 아이템의 내용을 숨기되 공간은 유지 */ -.cardWrapperDragging { - visibility: hidden; -} - -.todoCard { - flex: 1; - /* 접힘/펼침 전환 시 타이틀 수평 시프팅 방지: folded 상태와 패딩·보더 두께 통일 */ - padding-left: 20px !important; - border: 1px solid transparent !important; -} - -/* 피그마 fold=True 상태: 54px 높이, 좌측 패딩 20px, 테두리만 표시 */ -.todoCardFolded { - height: 54px !important; - padding: 0 12px 0 20px !important; - justify-content: center !important; - gap: 0 !important; - box-shadow: none !important; - border: 1px solid #e2e8f0 !important; -} - -.contextMenu { - position: absolute; - top: 40px; - right: 8px; - z-index: 10; - display: flex; - flex-direction: column; - background-color: var(--color-background-inverse); - border-radius: 8px; - box-shadow: - 0 4px 6px rgba(0, 0, 0, 0.1), - 0 2px 4px rgba(0, 0, 0, 0.06); - overflow: hidden; - min-width: 120px; -} - -.menuItem { - width: 100%; - padding: 12px 16px; - text-align: left; - font-size: 14px; - font-weight: 400; - color: var(--color-text-primary); - background: none; - border: none; - cursor: pointer; -} - -.menuItem:hover { - background-color: var(--color-background-tertiary); -} - -.editCard { - display: flex; - flex-direction: column; - gap: 8px; - padding: 12px; - background-color: var(--color-background-inverse); - border-radius: 8px; - border: 1px solid var(--color-brand-primary); -} - -.editTitleInput { - width: 100%; - padding: 6px 8px; - font-size: 14px; - font-weight: 500; - color: var(--color-text-primary); - background-color: var(--color-background-primary); - border: 1px solid var(--color-border-primary); - border-radius: 6px; - outline: none; - box-sizing: border-box; -} - -.editTitleInput:focus { - border-color: var(--color-brand-primary); -} - -.editItems { - display: flex; - flex-direction: column; - gap: 4px; -} - -.editItemInput { - width: 100%; - padding: 5px 8px; - font-size: 13px; - color: var(--color-text-primary); - background-color: var(--color-background-primary); - border: 1px solid var(--color-border-primary); - border-radius: 6px; - outline: none; - box-sizing: border-box; -} - -.editItemInput:focus { - border-color: var(--color-brand-primary); -} - -.editActions { - display: flex; - justify-content: flex-end; - gap: 8px; - margin-top: 4px; -} - -.cancelButton { - padding: 6px 12px; - font-size: 13px; - color: var(--color-text-secondary); - background: none; - border: 1px solid var(--color-border-primary); - border-radius: 6px; - cursor: pointer; -} - -.cancelButton:hover { - background-color: var(--color-background-tertiary); -} - -.saveButton { - padding: 6px 12px; - font-size: 13px; - color: var(--color-text-inverse); - background-color: var(--color-brand-primary); - border: none; - border-radius: 6px; - cursor: pointer; -} - -.saveButton:hover { - opacity: 0.85; -} diff --git a/src/app/[teamid]/_domain/components/Kanban/KanbanItem.tsx b/src/app/[teamid]/_domain/components/Kanban/KanbanItem.tsx deleted file mode 100644 index b34c9a8..0000000 --- a/src/app/[teamid]/_domain/components/Kanban/KanbanItem.tsx +++ /dev/null @@ -1,172 +0,0 @@ -'use client'; - -import { memo, useState, useRef, useEffect } from 'react'; -import { useSortable } from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; -import TodoCard from '@/components/todo-card/TodoCard'; -import styles from './KanbanItem.module.css'; -import type { KanbanTask, TaskItem } from '../../interfaces/team'; - -interface KanbanItemProps { - task: KanbanTask; - onItemCheckedChange?: (taskId: string, itemId: string, checked: boolean) => void; - onCardClick?: (taskId: string) => void; - onDeleteTask?: (taskId: string) => void; - onEditTask?: (taskId: string) => void; - onUpdateTask?: (taskId: string, updatedData: { title: string; items: TaskItem[] }) => void; -} - -function KanbanItem({ - task, - onItemCheckedChange, - onCardClick, - onDeleteTask, - onEditTask, - onUpdateTask, -}: KanbanItemProps) { - // 할일이 없거나 일부만 완료된 경우 펼침, 모두 완료된 경우 접힘 - const allChecked = task.items.length > 0 && task.items.every((item) => item.checked); - const [isExpanded, setIsExpanded] = useState(!allChecked); - const [isMenuOpen, setIsMenuOpen] = useState(false); - const [isEditing, setIsEditing] = useState(false); - const [editTitle, setEditTitle] = useState(task.title); - const [editItems, setEditItems] = useState(task.items); - const containerRef = useRef(null); - - const { listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id: task.id, - }); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - }; - - // 메뉴 외부 클릭 시 닫기 - useEffect(() => { - if (!isMenuOpen) return; - const handleClickOutside = (e: MouseEvent) => { - if (containerRef.current && !containerRef.current.contains(e.target as Node)) { - setIsMenuOpen(false); - } - }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, [isMenuOpen]); - - const handleContainerClick = (e: React.MouseEvent) => { - if (isEditing) return; - const target = e.target as HTMLElement; - if (!target.closest('button, input, label, a')) { - setIsExpanded((prev) => !prev); - } - }; - - const handleKebabClick = () => { - setIsMenuOpen((prev) => !prev); - }; - - const handleEdit = () => { - setIsMenuOpen(false); - setEditTitle(task.title); - setEditItems(task.items); - setIsEditing(true); - onEditTask?.(task.id); - }; - - const handleDelete = () => { - setIsMenuOpen(false); - onDeleteTask?.(task.id); - }; - - const handleSave = () => { - onUpdateTask?.(task.id, { title: editTitle, items: editItems }); - setIsEditing(false); - }; - - const handleCancel = () => { - setIsEditing(false); - }; - - const handleItemTextChange = (itemId: string, newText: string) => { - setEditItems((prev) => - prev.map((item) => (item.id === itemId ? { ...item, text: newText } : item)), - ); - }; - - return ( - // dnd-kit의 PointerSensor는 button 요소를 드래그 제외 대상으로 처리하므로 - // listeners를 item 컨테이너에 배치하여 카드 전체 영역에서 드래그 가능하게 함 -
-
- {isEditing ? ( -
- setEditTitle(e.target.value)} - placeholder="할 일 제목" - /> - {editItems.length > 0 && ( -
- {editItems.map((item) => ( - handleItemTextChange(item.id, e.target.value)} - placeholder="항목 내용" - /> - ))} -
- )} -
- - -
-
- ) : ( - <> - onItemCheckedChange(task.id, itemId, checked) - : undefined - } - onKebabClick={handleKebabClick} - className={isExpanded ? styles.todoCard : styles.todoCardFolded} - /> - {isMenuOpen && ( -
- - -
- )} - - )} -
-
- ); -} - -export default memo(KanbanItem); diff --git a/src/app/[teamid]/_domain/components/Member/MemberCard.module.css b/src/app/[teamid]/_domain/components/Member/MemberCard.module.css deleted file mode 100644 index 1d72e46..0000000 --- a/src/app/[teamid]/_domain/components/Member/MemberCard.module.css +++ /dev/null @@ -1,66 +0,0 @@ -.card { - display: flex; - align-items: center; - gap: 12px; - padding: 8px 0; - border-bottom: 1px solid var(--color-background-tertiary); -} - -.card:last-child { - border-bottom: none; - padding-bottom: 0; -} - -.kebab { - margin-left: auto; - flex-shrink: 0; -} - -.avatar { - width: 32px; - height: 32px; - border-radius: 50%; - background: var(--color-background-tertiary); - flex-shrink: 0; - overflow: hidden; - display: flex; - align-items: center; - justify-content: center; -} - -.avatarImg { - width: 100%; - height: 100%; - object-fit: cover; -} - -.avatarFallback { - font-size: 14px; - font-weight: 600; - color: var(--color-text-default); -} - -.info { - display: flex; - flex-direction: column; - gap: 2px; - min-width: 0; -} - -.name { - font-size: 13px; - font-weight: 600; - color: var(--color-text-primary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.email { - font-size: 12px; - font-weight: 400; - color: var(--color-text-default); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} diff --git a/src/app/[teamid]/_domain/components/Member/MemberCard.tsx b/src/app/[teamid]/_domain/components/Member/MemberCard.tsx deleted file mode 100644 index f5b0cdf..0000000 --- a/src/app/[teamid]/_domain/components/Member/MemberCard.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import Image from 'next/image'; -import MemberKebabMenu from './MemberKebabMenu'; -import styles from './MemberCard.module.css'; -import type { GroupMember } from '../../apis/types'; -import { useRemoveMemberMutation } from '../../queries/useRemoveMemberMutation'; - -interface MemberCardProps { - member: GroupMember; - isAdmin: boolean; - groupId: number; -} - -export default function MemberCard({ member, isAdmin, groupId }: MemberCardProps) { - const removeMemberMutation = useRemoveMemberMutation(groupId); - - const handleDelete = () => { - removeMemberMutation.mutate(member.userId); - }; - - return ( -
- -
- {member.userName} - {member.userEmail} -
- {isAdmin && ( -
- -
- )} -
- ); -} diff --git a/src/app/[teamid]/_domain/components/Member/MemberKebabMenu.module.css b/src/app/[teamid]/_domain/components/Member/MemberKebabMenu.module.css deleted file mode 100644 index c019ef0..0000000 --- a/src/app/[teamid]/_domain/components/Member/MemberKebabMenu.module.css +++ /dev/null @@ -1,63 +0,0 @@ -.container { - position: relative; - display: inline-block; -} - -.trigger { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - padding: 0; - background: none; - border: none; - cursor: pointer; - color: var(--color-icon-primary); - transition: color 0.2s; -} - -.trigger:hover { - color: var(--color-text-secondary); -} - -.icon { - font-size: 20px; - line-height: 1; - user-select: none; -} - -.dropdown { - position: absolute; - top: calc(100% + 4px); - right: 0; - min-width: 120px; - background: var(--color-background-primary); - border: 1px solid var(--color-border-primary); - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); - overflow: hidden; - z-index: 100; -} - -.menuItem { - display: block; - width: 100%; - padding: 12px 16px; - text-align: left; - font-size: 14px; - font-weight: 500; - color: var(--color-text-primary); - background: none; - border: none; - cursor: pointer; - transition: background-color 0.2s; -} - -.menuItem:hover { - background-color: var(--color-background-secondary); -} - -.menuItem:active { - background-color: var(--color-background-tertiary); -} diff --git a/src/app/[teamid]/_domain/components/Member/MemberKebabMenu.tsx b/src/app/[teamid]/_domain/components/Member/MemberKebabMenu.tsx deleted file mode 100644 index 8e6000d..0000000 --- a/src/app/[teamid]/_domain/components/Member/MemberKebabMenu.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client'; - -import { useState, useRef, useEffect } from 'react'; -import styles from './MemberKebabMenu.module.css'; - -interface MemberKebabMenuProps { - onDelete: () => void; -} - -export default function MemberKebabMenu({ onDelete }: MemberKebabMenuProps) { - const [isOpen, setIsOpen] = useState(false); - const menuRef = useRef(null); - - useEffect(() => { - if (!isOpen) return; - - const handleClickOutside = (e: MouseEvent) => { - if (menuRef.current && !menuRef.current.contains(e.target as Node)) { - setIsOpen(false); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, [isOpen]); - - const handleDelete = () => { - onDelete(); - setIsOpen(false); - }; - - return ( -
- - - {isOpen && ( -
- -
- )} -
- ); -} diff --git a/src/app/[teamid]/_domain/components/Member/MemberSection.module.css b/src/app/[teamid]/_domain/components/Member/MemberSection.module.css deleted file mode 100644 index e274f08..0000000 --- a/src/app/[teamid]/_domain/components/Member/MemberSection.module.css +++ /dev/null @@ -1,60 +0,0 @@ -.section { - background: var(--color-background-primary); - border-radius: 16px; - padding: 20px; - display: flex; - flex-direction: column; - gap: 12px; - margin-top: 44px; -} - -.header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; -} - -.title { - font-size: 14px; - font-weight: 600; - color: var(--color-text-primary); - margin: 0; -} - -.count { - color: var(--color-text-default); - font-weight: 400; -} - -.inviteButton { - background: none; - border: none; - cursor: pointer; - font-size: 14px; - font-weight: 600; - color: var(--color-brand-primary); - padding: 0; - font-family: inherit; - white-space: nowrap; -} - -.inviteButton:hover { - color: var(--color-interaction-hover); -} - -.list { - display: flex; - flex-direction: column; -} - -/* Toast 컴포넌트를 화면 상단 중앙에 위치시키기 위한 오버라이드 */ -.toastCenter { - left: calc(50% - 434px) !important; -} - -@media (max-width: 768px) { - .toastCenter { - left: calc(50% - 172px) !important; - } -} diff --git a/src/app/[teamid]/_domain/components/Member/MemberSection.tsx b/src/app/[teamid]/_domain/components/Member/MemberSection.tsx deleted file mode 100644 index 9554c09..0000000 --- a/src/app/[teamid]/_domain/components/Member/MemberSection.tsx +++ /dev/null @@ -1,81 +0,0 @@ -'use client'; - -import { useState, useCallback } from 'react'; -import MemberInvite from '@/components/Modal/domain/components/MemberInvite/MemberInvite'; -import Toast from '@/components/toast/Toast'; -import MemberCard from './MemberCard'; -import styles from './MemberSection.module.css'; -import type { GroupMember } from '../../apis/types'; -import { useGroupInvitationQuery } from '../../queries/useGroupInvitationQuery'; - -const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? ''; - -function buildInviteLink(inviteToken: string): string { - // 프론트엔드 도메인에서 초대 수락 경로로 이동하는 링크 생성 - if (typeof window !== 'undefined') { - return `${window.location.origin}/addteam/join?token=${inviteToken}`; - } - return `${BASE_URL}/addteam/join?token=${inviteToken}`; -} - -interface MemberSectionProps { - members: GroupMember[]; - isAdmin: boolean; - groupId: number; - teamId: string; -} - -export default function MemberSection({ members, isAdmin, groupId }: MemberSectionProps) { - const [isInviteOpen, setIsInviteOpen] = useState(false); - const [isToastOpen, setIsToastOpen] = useState(false); - - const { data: invitationData } = useGroupInvitationQuery(groupId, isInviteOpen); - - const inviteLink = invitationData ? buildInviteLink(invitationData) : ''; - - const handleCopyLink = useCallback((link: string) => { - navigator.clipboard.writeText(link).catch(() => {}); - setIsToastOpen(true); - }, []); - - return ( -
-
-

- 멤버 ({members.length}명) -

- -
- -
- {members.map((member) => ( - - ))} -
- - setIsInviteOpen(false)} - invite={{ - link: inviteLink, - onCopyLink: handleCopyLink, - }} - /> - - setIsToastOpen(false)} - actionLabel={null} - className={styles.toastCenter} - /> -
- ); -} diff --git a/src/app/[teamid]/_domain/components/Team/SidebarWrapper.tsx b/src/app/[teamid]/_domain/components/Team/SidebarWrapper.tsx deleted file mode 100644 index 36098c6..0000000 --- a/src/app/[teamid]/_domain/components/Team/SidebarWrapper.tsx +++ /dev/null @@ -1,22 +0,0 @@ -'use client'; - -import { Sidebar } from '@/components/sidebar'; -import ProfileImage from '@/components/profile-img/ProfileImage'; -import { useCurrentUserQuery } from '@/shared/queries/user/useCurrentUserQuery'; -import TeamSidebarDropdown from './TeamSidebarDropdown'; - -export default function SidebarWrapper() { - const { data: currentUser } = useCurrentUserQuery(); - - return ( - } - isLoggedIn={!!currentUser} - profileImage={ - - } - profileName={currentUser?.nickname} - profileTeam={currentUser?.email} - /> - ); -} diff --git a/src/app/[teamid]/_domain/components/Team/TeamDashboard.module.css b/src/app/[teamid]/_domain/components/Team/TeamDashboard.module.css deleted file mode 100644 index 07e4b36..0000000 --- a/src/app/[teamid]/_domain/components/Team/TeamDashboard.module.css +++ /dev/null @@ -1,52 +0,0 @@ -.container { - max-width: 1550px; - width: 100%; - display: flex; - flex-direction: column; - padding: 24px; - gap: 24px; - min-height: 100%; -} - -.content { - display: flex; - gap: 24px; - align-items: flex-start; -} - -.divider { - width: 100%; - height: 1px; - background: var(--Border-Primary, #e2e8f0); -} - -.kanbanArea { - flex: 1; - min-width: 0; -} - -.rightPanel { - width: 300px; - flex-shrink: 0; - display: flex; - flex-direction: column; - gap: 16px; -} - -@media (max-width: 1280px) { - .content { - flex-direction: column; - align-items: stretch; /* column 방향에서 자식이 전체 너비를 채우도록 */ - } - - .rightPanel { - width: 100%; - } -} - -@media (max-width: 767px) { - .container { - padding: 16px; - gap: 16px; - } -} diff --git a/src/app/[teamid]/_domain/components/Team/TeamDashboard.tsx b/src/app/[teamid]/_domain/components/Team/TeamDashboard.tsx deleted file mode 100644 index c66980f..0000000 --- a/src/app/[teamid]/_domain/components/Team/TeamDashboard.tsx +++ /dev/null @@ -1,137 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { useParams, useRouter } from 'next/navigation'; -import { useQuery, useQueries } from '@tanstack/react-query'; -import KanbanBoard from '../Kanban/KanbanBoard'; -import TodayReport from '../TodayReport/TodayReport'; -import MemberSection from '../Member/MemberSection'; -import WarningModal from '@/components/Modal/domain/components/WarningModal/WarningModal'; -import styles from './TeamDashboard.module.css'; -import { groupQueryOptions } from '../../queries/useGroupQuery'; -import { taskListQueryOptions } from '../../queries/useTaskListQuery'; -import { useCurrentUserQuery } from '@/shared/queries/user/useCurrentUserQuery'; -import { useDeleteGroupMutation } from '../../queries/useDeleteGroupMutation'; -import type { GroupMember } from '../../apis/types'; - -function getTodayDateString(): string { - return new Date().toISOString().split('T')[0]; -} - -function isGroupAdmin(members: GroupMember[] | undefined, userId: number): boolean { - return members?.some((m) => m.userId === userId && m.role === 'ADMIN') ?? false; -} - -function isTaskListDone(tasks: { doneAt: string | null }[]): boolean { - return tasks.length > 0 && tasks.every((t) => t.doneAt !== null); -} - -export default function TeamDashboard() { - const params = useParams<{ teamid: string }>(); - const router = useRouter(); - const teamid = params?.teamid ?? ''; - const groupId = Number(teamid); - - const { - data: group, - isPending, - isError, - } = useQuery({ - ...groupQueryOptions(groupId), - enabled: groupId > 0, - }); - const today = getTodayDateString(); - - const taskListQueries = useQueries({ - queries: (group?.taskLists ?? []).map((tl) => taskListQueryOptions(groupId, tl.id, today)), - }); - - const { data: currentUser } = useCurrentUserQuery(); - const { mutate: deleteGroup } = useDeleteGroupMutation(groupId); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isNavigating, setIsNavigating] = useState(false); - - if (isNaN(groupId) || groupId <= 0) { - return ( -
-

유효하지 않은 팀 ID입니다.

-
- ); - } - - if (isPending) { - return
로딩 중...
; - } - - if (isError || !group) { - if (isNavigating) return
; - return ( -
-

그룹을 찾을 수 없습니다.

-
- ); - } - - const totalTasks = taskListQueries.length; - const doneTasks = taskListQueries.filter((q) => isTaskListDone(q.data?.tasks ?? [])).length; - const isAdmin = currentUser ? isGroupAdmin(group.members, currentUser.id) : false; - - const handleConfirmDelete = () => { - // onSettled가 currentUser 쿼리를 invalidate하기 전에 미리 계산 - const remaining = (currentUser?.memberships ?? []) - .filter((m) => m.group.id !== groupId) - .map((m) => m.group); - - setIsDeleteModalOpen(false); - setIsNavigating(true); - - deleteGroup(undefined, { - onSuccess: () => { - router.push(remaining.length > 0 ? `/${remaining[0].id}` : '/addteam'); - }, - onError: () => { - setIsNavigating(false); - }, - }); - }; - - return ( -
- setIsDeleteModalOpen(true) : undefined} - /> - - setIsDeleteModalOpen(false)} - onConfirm={handleConfirmDelete} - text={{ - title: '팀을 삭제하시겠어요?', - description: '팀을 삭제하면 모든 데이터가\n영구적으로 삭제됩니다.', - closeLabel: '취소', - confirmLabel: '삭제하기', - }} - /> - -
- -
-
- -
- - -
-
- ); -} diff --git a/src/app/[teamid]/_domain/components/Team/TeamNavClient.module.css b/src/app/[teamid]/_domain/components/Team/TeamNavClient.module.css deleted file mode 100644 index 9255828..0000000 --- a/src/app/[teamid]/_domain/components/Team/TeamNavClient.module.css +++ /dev/null @@ -1,21 +0,0 @@ -.tabletWrapper { - display: none; -} - -.mobileWrapper { - display: none; -} - -/* 태블릿 */ -@media (min-width: 768px) and (max-width: 1279px) { - .tabletWrapper { - display: block; - } -} - -/* 모바일 */ -@media (max-width: 767px) { - .mobileWrapper { - display: block; - } -} diff --git a/src/app/[teamid]/_domain/components/Team/TeamNavClient.tsx b/src/app/[teamid]/_domain/components/Team/TeamNavClient.tsx deleted file mode 100644 index 6ed8eb0..0000000 --- a/src/app/[teamid]/_domain/components/Team/TeamNavClient.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import { useState } from 'react'; - -import { MobileHeader, MobileDrawer } from '@/components/sidebar'; -import ProfileImage from '@/components/profile-img/ProfileImage'; -import { useCurrentUserQuery } from '@/shared/queries/user/useCurrentUserQuery'; -import TeamTabletHeader from './TeamTabletHeader'; -import TeamSidebarDropdown from './TeamSidebarDropdown'; -import styles from './TeamNavClient.module.css'; - -export default function TeamNavClient() { - const [isDrawerOpen, setIsDrawerOpen] = useState(false); - const { data: currentUser } = useCurrentUserQuery(); - - const openDrawer = () => setIsDrawerOpen(true); - const closeDrawer = () => setIsDrawerOpen(false); - - return ( - <> - {/* 태블릿 헤더 */} -
- -
- - {/* 모바일 헤더 */} -
- - } - onMenuClick={openDrawer} - /> -
- - {/* 태블릿/모바일 공통 사이드바 드로어 */} - - - - - ); -} diff --git a/src/app/[teamid]/_domain/components/Team/TeamSidebarDropdown.module.css b/src/app/[teamid]/_domain/components/Team/TeamSidebarDropdown.module.css deleted file mode 100644 index 0955fce..0000000 --- a/src/app/[teamid]/_domain/components/Team/TeamSidebarDropdown.module.css +++ /dev/null @@ -1,130 +0,0 @@ -.container { - display: flex; - flex-direction: column; - width: 100%; - gap: 12px; -} - -/* 팀 선택 섹션 (트리거 + 팀 목록 + 추가 버튼) */ -.section { - display: flex; - flex-direction: column; - width: 100%; - gap: 8px; - padding-bottom: 12px; -} - -/* 팀 선택 헤더 트리거 */ -.trigger { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - padding: 8px 16px; - border-radius: 12px; - border: none; - background: transparent; - cursor: pointer; -} - -.triggerLeft { - display: flex; - align-items: center; - gap: 12px; -} - -.triggerLabel { - font-family: Pretendard, sans-serif; - font-weight: 600; - font-size: 16px; - line-height: 1.1875; - color: var(--color-text-disabled); -} - -.arrowOpen { - transform: rotate(180deg); -} - -/* 개별 팀 항목 */ -.teamItem { - display: flex; - align-items: center; - gap: 12px; - padding: 16px; - border-radius: 12px; - text-decoration: none; - font-family: Pretendard, sans-serif; - font-weight: 400; - font-size: 16px; - line-height: 1.1875; - color: var(--color-text-primary); - width: 100%; -} - -.teamItem:hover { - background: var(--color-brand-secondary); -} - -.active { - background: var(--color-brand-secondary); - color: var(--color-brand-primary); - font-weight: 600; -} - -.active img { - filter: brightness(0) saturate(100%) invert(45%) sepia(85%) saturate(1500%) hue-rotate(208deg) - brightness(102%) contrast(97%); -} - -/* 팀 추가하기 버튼 */ -.addButton { - display: flex; - align-items: center; - justify-content: center; - gap: 4px; - width: 100%; - padding: 8px 12px; - border-radius: 8px; - border: 1px solid var(--color-brand-primary); - background: transparent; - cursor: pointer; - font-family: Pretendard, sans-serif; - font-weight: 600; - font-size: 14px; - line-height: 1.2143; - color: var(--color-brand-primary); - text-decoration: none; -} - -.addIcon { - filter: brightness(0) saturate(100%) invert(45%) sepia(85%) saturate(1500%) hue-rotate(208deg) - brightness(102%) contrast(97%); -} - -/* 구분선 */ -.divider { - border: none; - border-top: 1px solid var(--color-background-tertiary); - margin: 0; - width: 100%; -} - -/* 자유게시판 */ -.boardItem { - display: flex; - align-items: center; - gap: 12px; - padding: 16px; - border-radius: 12px; - text-decoration: none; - font-family: Pretendard, sans-serif; - font-weight: 400; - font-size: 16px; - line-height: 1.1875; - color: var(--color-text-primary); - width: 100%; -} - -.boardItem:hover { - background: var(--color-brand-secondary); -} diff --git a/src/app/[teamid]/_domain/components/Team/TeamSidebarDropdown.tsx b/src/app/[teamid]/_domain/components/Team/TeamSidebarDropdown.tsx deleted file mode 100644 index 462b18c..0000000 --- a/src/app/[teamid]/_domain/components/Team/TeamSidebarDropdown.tsx +++ /dev/null @@ -1,83 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import Link from 'next/link'; -import Image from 'next/image'; -import { useParams } from 'next/navigation'; - -import styles from './TeamSidebarDropdown.module.css'; -import chessSmall from '@/assets/icons/chess/chessSmall.svg'; -import downArrowSmall from '@/assets/icons/arrow/downArrowSmall.svg'; -import plusSmall from '@/assets/icons/plus/plusSMall.svg'; -import boardSmall from '@/assets/icons/board/boardSmall.svg'; -import { useCurrentUserQuery } from '@/shared/queries/user/useCurrentUserQuery'; - -type Props = { - isCollapsed?: boolean; -}; - -export default function TeamSidebarDropdown({ isCollapsed }: Props) { - const params = useParams<{ teamid: string }>(); - const teamid = params?.teamid ?? ''; - const [isOpen, setIsOpen] = useState(true); - - const { data: currentUser } = useCurrentUserQuery(); - const teams = currentUser?.memberships.map((m) => m.group) ?? []; - - if (isCollapsed) return null; - - return ( -
- {/* 팀 선택 섹션 */} -
- - - {isOpen && ( - <> - {teams.map((team) => ( - - - {team.name} - - ))} - - - 팀 - 추가하기 - - - )} -
- - {/* 구분선 */} -
- - {/* 자유게시판 */} - - - 자유게시판 - -
- ); -} diff --git a/src/app/[teamid]/_domain/components/Team/TeamTabletHeader.module.css b/src/app/[teamid]/_domain/components/Team/TeamTabletHeader.module.css deleted file mode 100644 index 808d86d..0000000 --- a/src/app/[teamid]/_domain/components/Team/TeamTabletHeader.module.css +++ /dev/null @@ -1,30 +0,0 @@ -.tabletHeader { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - height: 60px; - padding: 0 16px; - background: var(--color-background-inverse); - border-bottom: 1px solid var(--color-background-tertiary); -} - -.hamburgerButton { - background: none; - border: none; - cursor: pointer; - padding: 0; - display: flex; - align-items: center; - justify-content: center; -} - -.logo { - display: flex; - align-items: center; -} - -.profileArea { - display: flex; - align-items: center; -} diff --git a/src/app/[teamid]/_domain/components/Team/TeamTabletHeader.tsx b/src/app/[teamid]/_domain/components/Team/TeamTabletHeader.tsx deleted file mode 100644 index 03d540a..0000000 --- a/src/app/[teamid]/_domain/components/Team/TeamTabletHeader.tsx +++ /dev/null @@ -1,36 +0,0 @@ -'use client'; - -import Image from 'next/image'; - -import ProfileImage from '@/components/profile-img/ProfileImage'; -import { useCurrentUserQuery } from '@/shared/queries/user/useCurrentUserQuery'; -import logoLarge from '@/assets/logos/logoLarge.svg'; -import hamburgerIcon from '@/assets/buttons/hamburger/hamburger.svg'; -import styles from './TeamTabletHeader.module.css'; - -type Props = { - onMenuClick?: () => void; -}; - -export default function TeamTabletHeader({ onMenuClick }: Props) { - const { data: currentUser } = useCurrentUserQuery(); - - return ( -
- -
- COWORKERS -
-
- -
-
- ); -} diff --git a/src/app/[teamid]/_domain/components/TodayReport/TodayReport.module.css b/src/app/[teamid]/_domain/components/TodayReport/TodayReport.module.css deleted file mode 100644 index c3bba23..0000000 --- a/src/app/[teamid]/_domain/components/TodayReport/TodayReport.module.css +++ /dev/null @@ -1,150 +0,0 @@ -.card { - width: 100%; - background: var(--color-background-primary); - border-radius: 20px; - box-shadow: 0px 15px 50px -12px rgba(0, 0, 0, 0.05); - padding: 32px 26px; - box-sizing: border-box; - display: flex; - flex-direction: column; - gap: 36px; -} - -.teamName { - font-size: 24px; - font-weight: 700; - color: var(--color-text-primary); - line-height: 1.167; - margin: 0; -} - -.content { - display: grid; - grid-template-columns: 1fr auto; - column-gap: 24px; - row-gap: 16px; -} - -.row { - grid-column: 1; - display: flex; - justify-content: space-between; - align-items: flex-end; -} - -.leftCol { - display: flex; - flex-direction: column; -} - -.progressLabel { - font-size: 14px; - font-weight: 500; - color: var(--color-interaction-inactive); - line-height: 1.214; -} - -.percent { - font-size: 40px; - font-weight: 700; - color: var(--color-brand-primary); - line-height: 1.193; -} - -.statsGroup { - display: flex; - align-items: center; - gap: 24px; -} - -.statItem { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; -} - -.statLabel { - font-size: 12px; - font-weight: 500; - color: var(--color-interaction-inactive); -} - -.statValue { - font-size: 32px; - font-weight: 700; - color: var(--color-text-default); - line-height: 1.193; -} - -.statValueDone { - color: var(--color-brand-primary); -} - -.divider { - width: 1px; - align-self: stretch; - background: var(--color-background-tertiary); -} - -.progressRow { - grid-column: 1 / -1; - display: grid; - grid-template-columns: subgrid; - align-items: center; -} - -.progressBar { - min-width: 0; -} - -.settingsWrapper { - position: relative; - display: inline-flex; - flex-shrink: 0; -} - -.settingsButton { - display: inline-flex; - align-items: center; - justify-content: center; - background: none; - border: none; - padding: 0; - cursor: pointer; -} - -.dropdown { - position: absolute; - top: calc(100% + 4px); - right: 0; - min-width: 120px; - background: var(--color-background-primary); - border: 1px solid var(--color-border-primary); - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); - overflow: hidden; - z-index: 100; -} - -.dropdownItem { - display: block; - width: 100%; - padding: 12px 16px; - text-align: left; - font-size: 14px; - font-weight: 500; - color: var(--color-text-primary); - background: none; - border: none; - cursor: pointer; - transition: background-color 0.2s; -} - -.dropdownItem:hover { - background-color: var(--color-background-secondary); -} - -.dropdownItem:active { - background-color: var(--color-background-tertiary); -} diff --git a/src/app/[teamid]/_domain/components/TodayReport/TodayReport.tsx b/src/app/[teamid]/_domain/components/TodayReport/TodayReport.tsx deleted file mode 100644 index acb09ef..0000000 --- a/src/app/[teamid]/_domain/components/TodayReport/TodayReport.tsx +++ /dev/null @@ -1,99 +0,0 @@ -'use client'; - -import { useState, useRef, useEffect } from 'react'; -import Image from 'next/image'; -import ProgressBar from '@/components/progressbar/ProgressBar'; -import SettingBig from '@/assets/icons/setting/SettingBig.svg'; -import styles from './TodayReport.module.css'; - -interface TodayReportProps { - teamName: string; - totalTasks: number; - doneTasks: number; - onDeleteGroup?: () => void; -} - -export default function TodayReport({ - teamName, - totalTasks, - doneTasks, - onDeleteGroup, -}: TodayReportProps) { - const progressPercent = totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0; - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const dropdownRef = useRef(null); - - useEffect(() => { - if (!isDropdownOpen) return; - - const handleClickOutside = (e: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { - setIsDropdownOpen(false); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, [isDropdownOpen]); - - const handleDeleteGroup = () => { - setIsDropdownOpen(false); - onDeleteGroup?.(); - }; - - return ( -
-

{teamName}

- -
-
-
- 오늘의 진행 상황 - {progressPercent}% -
- -
-
- 오늘의 할 일 - {totalTasks} -
- -
- -
- - {onDeleteGroup && ( -
- - {isDropdownOpen && ( -
- -
- )} -
- )} -
-
-
- ); -} diff --git a/src/app/[teamid]/_domain/constants/mockData.ts b/src/app/[teamid]/_domain/constants/mockData.ts deleted file mode 100644 index 33ca86d..0000000 --- a/src/app/[teamid]/_domain/constants/mockData.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { TeamMember, KanbanTask, MockTeam } from '../interfaces/team'; - -export const MOCK_TEAM_NAME = '경영관리팀'; - -export const MOCK_TEAMS: MockTeam[] = [ - { id: '1', name: '경영관리팀' }, - { id: '2', name: '프로덕트팀' }, - { id: '3', name: '마케팅팀' }, - { id: '4', name: '콘텐츠팀' }, -]; - -export const MOCK_MEMBERS: TeamMember[] = [ - { id: '1', name: '우지은', email: 'jieun@codeit.com' }, - { id: '2', name: '김민준', email: 'minjun@codeit.com' }, - { id: '3', name: '이서연', email: 'seoyeon@codeit.com' }, - { id: '4', name: '박도현', email: 'dohyeon@codeit.com' }, -]; - -export const MOCK_TASKS: KanbanTask[] = [ - { - id: 'task-1', - title: '법인 설립', - status: 'todo', - items: [ - { id: 'item-1', text: '법인 설립 안내 드리기', checked: false }, - { id: 'item-2', text: '법인 설립 혹은 변경 등기 비용 안내 드리기', checked: false }, - { id: 'item-3', text: '입력해주신 정보를 바탕으로 등기신청서 제출하기', checked: true }, - ], - }, - { - id: 'task-2', - title: '마케팅 전략 수립', - status: 'todo', - items: [ - { id: 'item-4', text: '시장 조사 보고서 작성', checked: false }, - { id: 'item-5', text: 'SNS 마케팅 계획 수립', checked: false }, - ], - }, - { - id: 'task-3', - title: '제품 기획', - status: 'inProgress', - items: [ - { id: 'item-6', text: '사용자 인터뷰 진행', checked: true }, - { id: 'item-7', text: '기능 명세서 작성', checked: true }, - { id: 'item-8', text: '와이어프레임 작성', checked: false }, - { id: 'item-9', text: '프로토타입 제작', checked: false }, - { id: 'item-10', text: '사용성 테스트', checked: false }, - ], - }, - { - id: 'task-4', - title: '회의록 정리', - status: 'done', - items: [ - { id: 'item-11', text: '주간 회의 내용 정리', checked: true }, - { id: 'item-12', text: '액션 아이템 배분', checked: true }, - { id: 'item-13', text: '팀원들에게 공유', checked: true }, - { id: 'item-14', text: '다음 회의 일정 잡기', checked: true }, - { id: 'item-15', text: '회의록 보관', checked: true }, - ], - }, -]; - -export const MOCK_TODAY_REPORT = { - totalTasks: 20, - doneTasks: 5, -}; - -export const MOCK_INVITE_LINK = 'https://coworkers.app/invite/abc123'; diff --git a/src/app/[teamid]/_domain/hooks/useKanbanDnd.ts b/src/app/[teamid]/_domain/hooks/useKanbanDnd.ts deleted file mode 100644 index 84a9914..0000000 --- a/src/app/[teamid]/_domain/hooks/useKanbanDnd.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { useState } from 'react'; -import { - PointerSensor, - useSensor, - useSensors, - type DragEndEvent, - type DragStartEvent, -} from '@dnd-kit/core'; -import { arrayMove } from '@dnd-kit/sortable'; -import type { KanbanTask, KanbanStatus } from '../interfaces/team'; - -// 드래그 시작으로 인식하는 최소 이동 거리(px) -const DRAG_ACTIVATION_DISTANCE = 8; - -export function useKanbanDnd( - tasks: KanbanTask[], - setTasks: React.Dispatch>, - columnIds: KanbanStatus[], - onStatusChange?: (taskId: string, fromStatus: KanbanStatus, toStatus: KanbanStatus) => void, -) { - const [activeTask, setActiveTask] = useState(null); - - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { distance: DRAG_ACTIVATION_DISTANCE }, - }), - ); - - const handleDragStart = (event: DragStartEvent) => { - setActiveTask(tasks.find((t) => t.id === String(event.active.id)) ?? null); - }; - - const handleDragEnd = (event: DragEndEvent) => { - const { active, over } = event; - setActiveTask(null); - - if (!over || active.id === over.id) return; - - const activeId = String(active.id); - const overId = String(over.id); - - // over가 컬럼인 경우 (빈 컬럼에 드롭) - const isOverColumn = columnIds.includes(overId as KanbanStatus); - if (isOverColumn) { - const activeTaskItem = tasks.find((t) => t.id === activeId); - if (activeTaskItem && activeTaskItem.status !== overId) { - const fromStatus = activeTaskItem.status; - const toStatus = overId as KanbanStatus; - setTasks((prev) => prev.map((t) => (t.id === activeId ? { ...t, status: toStatus } : t))); - onStatusChange?.(activeId, fromStatus, toStatus); - } - return; - } - - // over가 다른 태스크인 경우 - const overTaskItem = tasks.find((t) => t.id === overId); - const activeTaskItem = tasks.find((t) => t.id === activeId); - if (!overTaskItem || !activeTaskItem) return; - - if (activeTaskItem.status === overTaskItem.status) { - // 같은 컬럼 내 순서 변경 - setTasks((prev) => { - const colTasks = prev.filter((t) => t.status === activeTaskItem.status); - const otherTasks = prev.filter((t) => t.status !== activeTaskItem.status); - const oldIdx = colTasks.findIndex((t) => t.id === activeId); - const newIdx = colTasks.findIndex((t) => t.id === overId); - return [...otherTasks, ...arrayMove(colTasks, oldIdx, newIdx)]; - }); - } else { - // 다른 컬럼으로 이동 - const fromStatus = activeTaskItem.status; - const toStatus = overTaskItem.status; - setTasks((prev) => prev.map((t) => (t.id === activeId ? { ...t, status: toStatus } : t))); - onStatusChange?.(activeId, fromStatus, toStatus); - } - }; - - return { - activeTask, - sensors, - handleDragStart, - handleDragEnd, - }; -} diff --git a/src/app/[teamid]/_domain/hooks/useKanbanTasks.ts b/src/app/[teamid]/_domain/hooks/useKanbanTasks.ts deleted file mode 100644 index e8460f1..0000000 --- a/src/app/[teamid]/_domain/hooks/useKanbanTasks.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { useState, useCallback, useMemo } from 'react'; -import { useRouter } from 'next/navigation'; -import { useQueries, useQueryClient } from '@tanstack/react-query'; -import type { KanbanTask, KanbanStatus, TaskItem } from '../interfaces/team'; -import type { TaskList } from '../apis/types'; -import { taskListQueryOptions } from '../queries/useTaskListQuery'; -import { useCreateTaskListMutation } from '../queries/useCreateTaskListMutation'; -import { useDeleteTaskListMutation } from '../queries/useDeleteTaskListMutation'; -import { updateTask } from '../apis/task'; -import { taskListKeys } from '../queries/queryKeys'; - -function getTodayDateString(): string { - return new Date().toISOString().split('T')[0]; -} - -// localStorage에 컬럼 위치를 저장하여 새로고침 후에도 유지 -function getStoredStatus(groupId: number, taskListId: number): KanbanStatus | null { - try { - const stored = localStorage.getItem(`kanban-status-${groupId}-${taskListId}`); - if (stored === 'todo' || stored === 'inProgress' || stored === 'done') return stored; - return null; - } catch { - return null; - } -} - -function setStoredStatus(groupId: number, taskListId: number, status: KanbanStatus): void { - try { - localStorage.setItem(`kanban-status-${groupId}-${taskListId}`, status); - } catch {} -} - -function deriveStatus(items: TaskItem[]): KanbanStatus { - if (items.length === 0) return 'todo'; - const doneCount = items.filter((item) => item.checked).length; - if (doneCount === 0) return 'todo'; - if (doneCount === items.length) return 'done'; - return 'inProgress'; -} - -export function useKanbanTasks( - groupId: number, - teamId: string, - taskLists: Omit[], -) { - const router = useRouter(); - const queryClient = useQueryClient(); - const today = getTodayDateString(); - - // 각 할 일 목록의 태스크를 병렬로 조회 - const taskListQueries = useQueries({ - queries: taskLists.map((tl) => taskListQueryOptions(groupId, tl.id, today)), - }); - - // API 데이터 → KanbanTask 변환 (목록 ID와 쿼리 갱신 시각 기준으로 메모이제이션) - const taskListIds = taskLists.map((tl) => tl.id).join(','); - const queriesKey = taskListQueries.map((q) => q.dataUpdatedAt).join(','); - - const computedTasks = useMemo(() => { - return taskLists.map((tl, i) => { - const apiTasks = taskListQueries[i]?.data?.tasks ?? []; - const items: TaskItem[] = apiTasks.map((task) => ({ - id: String(task.id), - text: task.name, - checked: task.doneAt !== null, - })); - // localStorage 저장값 우선, 없으면 item 완료 비율로 파생 - const storedStatus = getStoredStatus(groupId, tl.id); - return { - id: String(tl.id), - title: tl.name, - items, - status: storedStatus ?? deriveStatus(items), - }; - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [taskListIds, queriesKey]); - - // DnD를 위한 로컬 상태 (API 데이터 변경 시 초기화) - const [prevComputed, setPrevComputed] = useState(computedTasks); - const [tasks, setTasks] = useState(computedTasks); - - if (prevComputed !== computedTasks) { - setPrevComputed(computedTasks); - setTasks(computedTasks); - } - - // 할 일 목록 추가 모달 상태 - const [addingStatus, setAddingStatus] = useState(null); - const [newListTitle, setNewListTitle] = useState(''); - - const createTaskListMutation = useCreateTaskListMutation(groupId); - const deleteTaskListMutation = useDeleteTaskListMutation(groupId); - - // 카드 클릭 시 할 일 목록 상세 페이지로 이동 - const handleCardClick = useCallback( - (taskId: string) => { - router.push(`/${teamId}/tasks/${taskId}`); - }, - [router, teamId], - ); - - // 할 일 목록 삭제 - const handleDeleteTask = useCallback( - (taskId: string) => { - deleteTaskListMutation.mutate(Number(taskId)); - }, - [deleteTaskListMutation], - ); - - // 태스크 아이템 체크 상태 변경은 할 일 목록 상세 페이지에서 처리하므로 빈 함수 - const handleItemCheckedChange = useCallback(() => {}, []); - - // 할 일 목록 추가 모달 열기 - const handleAddTask = useCallback((status: KanbanStatus) => { - setAddingStatus(status); - }, []); - - // 새 할 일 목록 생성 - const handleAddListSubmit = useCallback(async () => { - if (!newListTitle.trim() || !addingStatus) return; - try { - await createTaskListMutation.mutateAsync(newListTitle.trim()); - setNewListTitle(''); - setAddingStatus(null); - } catch { - // 에러는 상위에서 처리 - } - }, [newListTitle, addingStatus, createTaskListMutation]); - - const handleAddListClose = useCallback(() => { - setAddingStatus(null); - setNewListTitle(''); - }, []); - - const handleNewListTitleChange = useCallback((value: string) => { - setNewListTitle(value); - }, []); - - // 수정 기능은 할 일 목록 상세 페이지에서 처리 - const handleUpdateTask = useCallback(() => {}, []); - - // 드래그로 컬럼 이동 시 컬럼 위치를 저장하고, 완료/할 일 이동 시 API로 완료 상태 동기화 - const handleStatusChange = useCallback( - async (taskId: string, fromStatus: KanbanStatus, toStatus: KanbanStatus) => { - if (fromStatus === toStatus) return; - - const task = tasks.find((t) => t.id === taskId); - const taskListId = Number(taskId); - - // 항목 유무와 관계없이 컬럼 위치를 localStorage에 저장 - setStoredStatus(groupId, taskListId, toStatus); - - // 진행중으로 이동하거나 항목이 없으면 API 호출 없이 종료 (위치는 이미 저장됨) - if (!task || task.items.length === 0 || toStatus === 'inProgress') return; - - try { - if (toStatus === 'done') { - // 모든 항목 완료 처리 - await Promise.all( - task.items.map((item) => - updateTask(groupId, taskListId, Number(item.id), { done: true }), - ), - ); - } else if (toStatus === 'todo') { - // 모든 항목 미완료 처리 - await Promise.all( - task.items.map((item) => - updateTask(groupId, taskListId, Number(item.id), { done: false }), - ), - ); - } - } finally { - // 성공/실패 관계없이 쿼리를 무효화하여 실제 서버 상태로 동기화 - await queryClient.invalidateQueries({ - queryKey: taskListKeys.detail(groupId, taskListId, today), - }); - } - }, - [tasks, groupId, today, queryClient], - ); - - return { - tasks, - setTasks, - addingStatus, - newListTitle, - handleItemCheckedChange, - handleCardClick, - handleDeleteTask, - handleUpdateTask, - handleStatusChange, - handleAddTask, - handleAddListSubmit, - handleAddListClose, - handleNewListTitleChange, - }; -} diff --git a/src/app/[teamid]/_domain/interfaces/team.ts b/src/app/[teamid]/_domain/interfaces/team.ts deleted file mode 100644 index 8e04562..0000000 --- a/src/app/[teamid]/_domain/interfaces/team.ts +++ /dev/null @@ -1,26 +0,0 @@ -export type KanbanStatus = 'todo' | 'inProgress' | 'done'; - -export interface TeamMember { - id: string; - name: string; - email: string; - imageUrl?: string; -} - -export interface TaskItem { - id: string; - text: string; - checked: boolean; -} - -export interface KanbanTask { - id: string; - title: string; - items: TaskItem[]; - status: KanbanStatus; -} - -export interface MockTeam { - id: string; - name: string; -} diff --git a/src/app/[teamid]/_domain/queries/queryKeys.ts b/src/app/[teamid]/_domain/queries/queryKeys.ts deleted file mode 100644 index b87a011..0000000 --- a/src/app/[teamid]/_domain/queries/queryKeys.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const teamGroupKeys = { - all: ['groups'] as const, - details: () => [...teamGroupKeys.all, 'detail'] as const, - detail: (groupId: number) => [...teamGroupKeys.details(), groupId] as const, - tasks: (groupId: number, date?: string) => - [...teamGroupKeys.all, groupId, 'tasks', date] as const, - invitation: (groupId: number) => [...teamGroupKeys.all, groupId, 'invitation'] as const, -}; - -export const taskListKeys = { - detail: (groupId: number, taskListId: number, date?: string) => - ['groups', groupId, 'task-lists', taskListId, date] as const, -}; diff --git a/src/app/[teamid]/_domain/queries/useCreateTaskListMutation.ts b/src/app/[teamid]/_domain/queries/useCreateTaskListMutation.ts deleted file mode 100644 index 51fe2cb..0000000 --- a/src/app/[teamid]/_domain/queries/useCreateTaskListMutation.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { createTaskList } from '../apis/taskList'; -import { teamGroupKeys } from './queryKeys'; - -export function useCreateTaskListMutation(groupId: number) { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (name: string) => createTaskList(groupId, name), - onSettled: () => { - void queryClient.invalidateQueries({ queryKey: teamGroupKeys.detail(groupId) }); - }, - }); -} diff --git a/src/app/[teamid]/_domain/queries/useDeleteGroupMutation.ts b/src/app/[teamid]/_domain/queries/useDeleteGroupMutation.ts deleted file mode 100644 index b18827f..0000000 --- a/src/app/[teamid]/_domain/queries/useDeleteGroupMutation.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { deleteGroup } from '../apis/group'; -import { teamGroupKeys } from './queryKeys'; -import { currentUserKeys } from '@/shared/queries/user/useCurrentUserQuery'; - -export function useDeleteGroupMutation(groupId: number) { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: () => deleteGroup(groupId), - onSettled: () => { - void queryClient.invalidateQueries({ queryKey: teamGroupKeys.all }); - // 유저 멤버십 목록도 갱신하여 사이드바에서 삭제된 팀이 사라지도록 처리 - void queryClient.invalidateQueries({ queryKey: currentUserKeys.all }); - }, - }); -} diff --git a/src/app/[teamid]/_domain/queries/useDeleteTaskListMutation.ts b/src/app/[teamid]/_domain/queries/useDeleteTaskListMutation.ts deleted file mode 100644 index d7610c8..0000000 --- a/src/app/[teamid]/_domain/queries/useDeleteTaskListMutation.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { deleteTaskList } from '../apis/taskList'; -import { teamGroupKeys } from './queryKeys'; - -export function useDeleteTaskListMutation(groupId: number) { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (taskListId: number) => deleteTaskList(groupId, taskListId), - onSettled: () => { - void queryClient.invalidateQueries({ queryKey: teamGroupKeys.detail(groupId) }); - }, - }); -} diff --git a/src/app/[teamid]/_domain/queries/useGroupInvitationQuery.ts b/src/app/[teamid]/_domain/queries/useGroupInvitationQuery.ts deleted file mode 100644 index b4701f4..0000000 --- a/src/app/[teamid]/_domain/queries/useGroupInvitationQuery.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { queryOptions, useQuery } from '@tanstack/react-query'; -import { getGroupInvitation } from '../apis/group'; -import { teamGroupKeys } from './queryKeys'; - -export function groupInvitationQueryOptions(groupId: number) { - return queryOptions({ - queryKey: teamGroupKeys.invitation(groupId), - queryFn: () => getGroupInvitation(groupId), - staleTime: 0, - }); -} - -export function useGroupInvitationQuery(groupId: number, enabled: boolean) { - return useQuery({ - ...groupInvitationQueryOptions(groupId), - enabled, - }); -} diff --git a/src/app/[teamid]/_domain/queries/useGroupQuery.ts b/src/app/[teamid]/_domain/queries/useGroupQuery.ts deleted file mode 100644 index a46cf21..0000000 --- a/src/app/[teamid]/_domain/queries/useGroupQuery.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'; -import { getGroup } from '../apis/group'; -import { teamGroupKeys } from './queryKeys'; - -export function groupQueryOptions(groupId: number) { - return queryOptions({ - queryKey: teamGroupKeys.detail(groupId), - queryFn: () => getGroup(groupId), - }); -} - -export function useGroupQuery(groupId: number) { - return useSuspenseQuery(groupQueryOptions(groupId)); -} diff --git a/src/app/[teamid]/_domain/queries/useGroupTasksQuery.ts b/src/app/[teamid]/_domain/queries/useGroupTasksQuery.ts deleted file mode 100644 index 9e34039..0000000 --- a/src/app/[teamid]/_domain/queries/useGroupTasksQuery.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { queryOptions, useQuery } from '@tanstack/react-query'; -import { getGroupTasks } from '../apis/group'; -import { teamGroupKeys } from './queryKeys'; - -export function groupTasksQueryOptions(groupId: number, date?: string) { - return queryOptions({ - queryKey: teamGroupKeys.tasks(groupId, date), - queryFn: () => getGroupTasks(groupId, date), - }); -} - -export function useGroupTasksQuery(groupId: number, date?: string) { - return useQuery(groupTasksQueryOptions(groupId, date)); -} diff --git a/src/app/[teamid]/_domain/queries/useRemoveMemberMutation.ts b/src/app/[teamid]/_domain/queries/useRemoveMemberMutation.ts deleted file mode 100644 index cf5fc33..0000000 --- a/src/app/[teamid]/_domain/queries/useRemoveMemberMutation.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { removeGroupMember } from '@/shared/apis/groups/member'; -import { teamGroupKeys } from './queryKeys'; - -export function useRemoveMemberMutation(groupId: number) { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (memberUserId: number) => removeGroupMember(groupId, memberUserId), - onSettled: () => { - void queryClient.invalidateQueries({ queryKey: teamGroupKeys.detail(groupId) }); - }, - }); -} diff --git a/src/app/[teamid]/_domain/queries/useTaskListQuery.ts b/src/app/[teamid]/_domain/queries/useTaskListQuery.ts deleted file mode 100644 index d834397..0000000 --- a/src/app/[teamid]/_domain/queries/useTaskListQuery.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { queryOptions, useQuery } from '@tanstack/react-query'; -import { getTaskList } from '../apis/taskList'; -import { taskListKeys } from './queryKeys'; - -export function taskListQueryOptions(groupId: number, taskListId: number, date?: string) { - return queryOptions({ - queryKey: taskListKeys.detail(groupId, taskListId, date), - queryFn: () => getTaskList(groupId, taskListId, date), - }); -} - -export function useTaskListQuery(groupId: number, taskListId: number, date?: string) { - return useQuery(taskListQueryOptions(groupId, taskListId, date)); -} diff --git a/src/app/[teamid]/_domain/queries/useUpdateGroupMutation.ts b/src/app/[teamid]/_domain/queries/useUpdateGroupMutation.ts deleted file mode 100644 index 994eac9..0000000 --- a/src/app/[teamid]/_domain/queries/useUpdateGroupMutation.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { updateGroup } from '../apis/group'; -import type { Group } from '../apis/types'; -import { teamGroupKeys } from './queryKeys'; - -export function useUpdateGroupMutation(groupId: number) { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (body: Parameters[1]) => updateGroup(groupId, body), - onSuccess: (updatedGroup) => { - queryClient.setQueryData(teamGroupKeys.detail(groupId), (prev: Group | undefined) => - prev ? { ...prev, ...updatedGroup } : updatedGroup, - ); - }, - onSettled: () => { - void queryClient.invalidateQueries({ queryKey: teamGroupKeys.detail(groupId) }); - }, - }); -} diff --git a/src/app/[teamid]/_domain/styles/common.module.css b/src/app/[teamid]/_domain/styles/common.module.css deleted file mode 100644 index e9f86bb..0000000 --- a/src/app/[teamid]/_domain/styles/common.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.flexCol { - display: flex; - flex-direction: column; -} - -.flexColCenter { - display: flex; - flex-direction: column; - align-items: center; -} - -.flexCenter { - display: flex; - align-items: center; - justify-content: center; -} diff --git a/src/app/[teamid]/layout.tsx b/src/app/[teamid]/layout.tsx deleted file mode 100644 index 53ca7db..0000000 --- a/src/app/[teamid]/layout.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import SidebarWrapper from './_domain/components/Team/SidebarWrapper'; -import TeamNavClient from './_domain/components/Team/TeamNavClient'; -import styles from './page.module.css'; - -export default function TeamLayout({ children }: { children: React.ReactNode }) { - return ( -
-
- -
- -
{children}
-
- ); -} diff --git a/src/app/[teamid]/page.module.css b/src/app/[teamid]/page.module.css deleted file mode 100644 index 6ea7c63..0000000 --- a/src/app/[teamid]/page.module.css +++ /dev/null @@ -1,99 +0,0 @@ -.page { - display: flex; - min-height: 100vh; - background: var(--color-background-secondary); -} - -.mainContents { - flex: 1; - display: flex; - flex-direction: column; - overflow-y: auto; - overflow-x: hidden; /* Added to prevent horizontal scroll */ - min-width: 0; - width: 100%; /* Explicitly set width to 100% */ - box-sizing: border-box; /* Ensure padding is included in the width */ - margin-left: 24px; -} - -/* Apply max-width to direct children of mainContents to prevent overflow */ -.mainContents > * { - max-width: 100%; - box-sizing: border-box; -} - -/* Default styles (Desktop) */ -.desktopSidebar { - display: block; /* Show desktop sidebar */ - /* fixed 포지션 사이드바가 flex 흐름에서 공간을 차지하지 않으므로, spacer 역할을 위해 너비 명시 */ - width: 270px; - min-width: 270px; - flex-shrink: 0; -} -/* Tablet styles */ -@media (min-width: 768px) and (max-width: 1279px) { - .page { - flex-direction: column; - } - - .desktopSidebar { - display: none; - } - - .mainContents { - margin-left: 0; - } -} - -/* Mobile styles */ -@media (max-width: 767px) { - .page { - flex-direction: column; - } - - .desktopSidebar { - display: none; - } - - .mainContents { - width: 100%; - margin-left: 0; - } -} - -.kanbanResponsiveContainer { - display: flex; - gap: 16px; /* Space between columns */ - width: 100%; /* Ensure it takes full width */ - box-sizing: border-box; - overflow-x: auto; /* Allow horizontal scroll if columns don't fit */ - padding-bottom: 8px; /* For scrollbar visibility */ -} - -/* Tablet specific rules for kanbanResponsiveContainer */ -@media (min-width: 768px) and (max-width: 1279px) { - .kanbanResponsiveContainer { - flex-wrap: nowrap; /* Prevent wrapping on tablet */ - justify-content: space-between; /* Distribute items evenly */ - } - - .kanbanResponsiveContainer > * { - /* Target direct children (Kanban columns) */ - flex: 1 1 auto; /* Allow items to grow and shrink */ - min-width: 0; /* Crucial for flex items to shrink below their content size */ - max-width: calc(33.33% - 12px); /* Distribute width for 3 columns, accounting for gap */ - } -} - -/* Mobile specific rules for kanbanResponsiveContainer */ -@media (max-width: 767px) { - .kanbanResponsiveContainer { - flex-wrap: wrap; /* Always wrap on mobile */ - justify-content: center; - } - - .kanbanResponsiveContainer > * { - /* Target direct children (Kanban columns) */ - width: 100%; /* Each column takes full width */ - } -} diff --git a/src/app/[teamid]/page.tsx b/src/app/[teamid]/page.tsx deleted file mode 100644 index 88b0c18..0000000 --- a/src/app/[teamid]/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -'use client'; - -import dynamic from 'next/dynamic'; -import { useParams } from 'next/navigation'; - -const TeamDashboard = dynamic(() => import('./_domain/components/Team/TeamDashboard'), { - ssr: false, -}); - -export default function TeamPage() { - const params = useParams<{ teamid: string }>(); - - return ; -} diff --git a/src/app/addteam/_domain/apis/image.ts b/src/app/addteam/_domain/apis/image.ts deleted file mode 100644 index bb96b66..0000000 --- a/src/app/addteam/_domain/apis/image.ts +++ /dev/null @@ -1 +0,0 @@ -export { uploadImage } from '@/shared/apis/images'; diff --git a/src/app/addteam/_domain/components/CreateTeamCard.module.css b/src/app/addteam/_domain/components/CreateTeamCard.module.css deleted file mode 100644 index 89802e6..0000000 --- a/src/app/addteam/_domain/components/CreateTeamCard.module.css +++ /dev/null @@ -1,85 +0,0 @@ -.title { - margin: 0; - color: var(--color-text-primary); - font-size: 24px; - font-weight: 700; - line-height: 28px; -} - -.label { - margin: 0; - color: var(--color-text-secondary); - font-size: 16px; - font-weight: 500; - line-height: 19px; -} - -.card { - position: relative; - width: var(--create-card-width-desktop); - padding: 32px 28px; - gap: 20px; - border-radius: 24px; - box-shadow: 4px 4px 10px 0 #24242440; - background-color: var(--color-background-inverse); -} - -.profileSection { - gap: 12px; -} - -.inputSection { - gap: 8px; -} - -.teamNameInput { - height: var(--control-height-desktop); -} - -.submitButton { - margin-top: 40px; - height: var(--control-height-desktop); -} - -@media (max-width: 767px) { - .title { - font-size: 20px; - line-height: 24px; - } - - .label { - color: var(--color-text-primary); - font-size: 14px; - line-height: 17px; - } - - .card { - width: var(--create-card-width-mobile); - min-height: 464px; - padding: 24px 22px; - gap: 0; - border-radius: 24px; - } - - .profileSection { - margin-top: 34px; - } - - .inputSection { - margin-top: 24px; - gap: 8px; - } - - .teamNameInput { - height: var(--control-height-mobile); - padding: 12px; - font-size: 14px; - line-height: 17px; - } - - .submitButton { - margin-top: 40px; - height: var(--control-height-mobile); - font-size: 14px; - } -} diff --git a/src/app/addteam/_domain/components/CreateTeamCard.tsx b/src/app/addteam/_domain/components/CreateTeamCard.tsx deleted file mode 100644 index ba39122..0000000 --- a/src/app/addteam/_domain/components/CreateTeamCard.tsx +++ /dev/null @@ -1,64 +0,0 @@ -'use client'; - -import { type FormEvent } from 'react'; -import { BaseButton } from '@/components/Button/base'; -import { Input } from '@/components/input'; -import { ProfileImage } from '@/components/profile-img'; -import type { CreateTeamFeedback } from '../interfaces/feedback'; -import FeedbackMessage from './FeedbackMessage'; -import cardStyles from './CreateTeamCard.module.css'; -import clsx from 'clsx'; -import commonStyles from '../styles/common.module.css'; - -const CREATE_TEAM_FEEDBACK_ID = 'create-team-feedback'; - -interface CreateTeamCardProps { - value: string; - disabled: boolean; - feedback: CreateTeamFeedback | null; - onChange: (value: string) => void; - onSubmit: () => void | Promise; -} - -export default function CreateTeamCard({ - value, - disabled, - feedback, - onChange, - onSubmit, -}: CreateTeamCardProps) { - const handleFormSubmit = (event: FormEvent) => { - event.preventDefault(); - void onSubmit(); - }; - - return ( -
-

팀 생성하기

- -
- -
- -
- - onChange(event.target.value)} - aria-describedby={CREATE_TEAM_FEEDBACK_ID} - placeholder="팀 이름을 입력해주세요" - className={cardStyles.teamNameInput} - /> -
- - - 생성하기 - - - - - ); -} diff --git a/src/app/addteam/_domain/components/FeedbackMessage.module.css b/src/app/addteam/_domain/components/FeedbackMessage.module.css deleted file mode 100644 index ff30b87..0000000 --- a/src/app/addteam/_domain/components/FeedbackMessage.module.css +++ /dev/null @@ -1,37 +0,0 @@ -.helperText { - margin: 0; - color: var(--color-text-default); - font-size: 14px; - line-height: 1.5; - text-align: center; -} - -.errorText { - margin: 0; - color: var(--color-status-danger); - font-size: 14px; - font-weight: 600; - line-height: 1.5; - text-align: center; -} - -.successText { - margin: 0; - color: var(--color-brand-primary); - font-size: 14px; - line-height: 1.5; - text-align: center; -} - -@media (max-width: 767px) { - .helperText, - .errorText, - .successText { - margin-top: 16px; - } - - .helperText { - font-size: 12px; - line-height: 14px; - } -} diff --git a/src/app/addteam/_domain/components/FeedbackMessage.tsx b/src/app/addteam/_domain/components/FeedbackMessage.tsx deleted file mode 100644 index 454dc1e..0000000 --- a/src/app/addteam/_domain/components/FeedbackMessage.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { CreateTeamFeedback } from '../interfaces/feedback'; -import feedbackMessageStyles from './FeedbackMessage.module.css'; - -interface FeedbackMessageProps { - id: string; - createTeamFeedback: CreateTeamFeedback | null; -} - -export default function FeedbackMessage({ id, createTeamFeedback }: FeedbackMessageProps) { - if (!createTeamFeedback) { - return ( -

- 팀 이름은 회사명이나 모임 이름 등으로 설정하면 좋아요. -

- ); - } - - if (createTeamFeedback.type === 'error') { - return ( - - ); - } - - return ( -

- {createTeamFeedback.message} -

- ); -} diff --git a/src/app/addteam/_domain/components/JoinTeamCard.module.css b/src/app/addteam/_domain/components/JoinTeamCard.module.css deleted file mode 100644 index 1ebdd5b..0000000 --- a/src/app/addteam/_domain/components/JoinTeamCard.module.css +++ /dev/null @@ -1,99 +0,0 @@ -.title { - margin: 0; - color: var(--color-text-primary); - font-size: 24px; - font-weight: 700; - line-height: 28px; -} - -.joinLabel { - margin: 0; - color: var(--color-text-primary); - font-size: 16px; - font-weight: 500; - line-height: 19px; -} - -.joinCard { - position: relative; - display: inline-flex; - justify-content: center; - width: var(--join-card-width-desktop); - height: 400px; - padding: 59px 45px 63px; - border-radius: 20px; - background: var(--color-background-inverse); -} - -.joinInputSection { - width: var(--join-input-width-desktop); - margin-top: 40px; - gap: 8px; -} - -.teamLinkInput { - width: 100%; - height: var(--control-height-desktop); -} - -.joinSubmitButton { - width: var(--join-input-width-desktop); - height: var(--control-height-desktop); - margin-top: 40px; - border-radius: 12px; -} - -.joinHelperText { - margin: 24px 0 0; - color: var(--color-text-default); - font-size: 16px; - line-height: 19px; - text-align: center; -} - -@media (max-width: 767px) { - .joinCard { - align-items: stretch; - justify-content: flex-start; - width: var(--create-card-width-mobile); - min-height: 264px; - padding: 24px 22px 28px; - border-radius: 24px; - } - - .joinInputSection { - width: 100%; - margin-top: 24px; - gap: 8px; - } - - .title { - font-size: 20px; - line-height: 24px; - } - - .joinLabel { - font-size: 14px; - line-height: 17px; - } - - .teamLinkInput { - height: var(--control-height-mobile); - font-size: 14px; - line-height: 17px; - } - - .joinSubmitButton { - width: 100%; - height: var(--control-height-mobile); - margin-top: 40px; - font-size: 14px; - line-height: 17px; - } - - .joinHelperText { - margin-top: 16px; - font-size: 12px; - line-height: 14px; - } -} diff --git a/src/app/addteam/_domain/components/JoinTeamCard.tsx b/src/app/addteam/_domain/components/JoinTeamCard.tsx deleted file mode 100644 index d062f30..0000000 --- a/src/app/addteam/_domain/components/JoinTeamCard.tsx +++ /dev/null @@ -1,86 +0,0 @@ -'use client'; - -import { type FormEvent } from 'react'; -import { BaseButton } from '@/components/Button/base'; -import { Input } from '@/components/input'; -import type { CreateTeamFeedback } from '../interfaces/feedback'; -import joinCardStyles from './JoinTeamCard.module.css'; -import feedbackStyles from './FeedbackMessage.module.css'; -import clsx from 'clsx'; -import commonStyles from '../styles/common.module.css'; - -const JOIN_TEAM_FEEDBACK_ID = 'join-team-helper-text'; - -interface JoinTeamCardProps { - teamLink: string; - disabled: boolean; - feedback: CreateTeamFeedback | null; - onTeamLinkChange: (value: string) => void; - onSubmit: () => void | Promise; -} - -export default function JoinTeamCard({ - teamLink, - disabled, - feedback, - onTeamLinkChange, - onSubmit, -}: JoinTeamCardProps) { - const handleFormSubmit = (event: FormEvent) => { - event.preventDefault(); - void onSubmit(); - }; - - return ( -
-

팀 참여하기

- -
- - onTeamLinkChange(event.target.value)} - aria-describedby={JOIN_TEAM_FEEDBACK_ID} - placeholder="팀 링크를 입력해주세요." - className={joinCardStyles.teamLinkInput} - /> -
- - - 참여하기 - - - {feedback ? ( - feedback.type === 'error' ? ( - - ) : ( -

- {feedback.message} -

- ) - ) : ( -

- 공유받은 팀 링크를 입력해 참여할 수 있어요. -

- )} -
- ); -} diff --git a/src/app/addteam/_domain/components/NoTeamState.module.css b/src/app/addteam/_domain/components/NoTeamState.module.css deleted file mode 100644 index 15cc22a..0000000 --- a/src/app/addteam/_domain/components/NoTeamState.module.css +++ /dev/null @@ -1,58 +0,0 @@ -.noTeamState { - width: min(90vw, 660px); -} - -.noTeamIllustration { - width: min(100%, 660px); - height: auto; -} - -.noTeamMessage { - margin: 20px 0 0; - color: var(--color-text-default); - font-size: 16px; - font-weight: 500; - line-height: 34px; - text-align: center; -} - -.noTeamActions { - width: min(100%, 370px); - margin-top: 48px; - gap: 16px; -} - -.noTeamActionButton { - height: var(--control-height-desktop); -} - -@media (max-width: 767px) { - .noTeamState { - width: 100%; - max-width: 343px; - } - - .noTeamIllustration { - width: min(100%, 343px); - } - - .noTeamMessage { - margin-top: 16px; - color: var(--color-text-default); - font-size: 14px; - font-weight: 500; - line-height: 24px; - } - - .noTeamActions { - width: 100%; - max-width: 343px; - margin-top: 40px; - } - - .noTeamActionButton { - height: var(--control-height-mobile); - font-size: 14px; - line-height: 17px; - } -} diff --git a/src/app/addteam/_domain/components/NoTeamState.tsx b/src/app/addteam/_domain/components/NoTeamState.tsx deleted file mode 100644 index 23652e1..0000000 --- a/src/app/addteam/_domain/components/NoTeamState.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client'; - -import Image from 'next/image'; -import { useRouter } from 'next/navigation'; -import { BaseButton } from '@/components/Button/base'; -import noTeamImage from '../svg/noTeamImg.svg'; -import noTeamStateStyles from './NoTeamState.module.css'; -import clsx from 'clsx'; -import commonStyles from '../styles/common.module.css'; - -export default function NoTeamState() { - const router = useRouter(); - - return ( -
- 소속된 팀이 없는 상태 일러스트 -

- 아직 소속된 팀이 없습니다. -
- 팀을 생성하거나 팀에 참여해보세요. -

-
- router.push('/addteam/create')} - > - 팀 생성하기 - - router.push('/addteam/join')} - > - 팀 참가하기 - -
-
- ); -} diff --git a/src/app/addteam/_domain/constants/createTeam.ts b/src/app/addteam/_domain/constants/createTeam.ts deleted file mode 100644 index a7b1864..0000000 --- a/src/app/addteam/_domain/constants/createTeam.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const CREATE_TEAM_MESSAGES = { - success: '팀이 생성되었습니다.', - emptyTeamNameError: '팀 이름을 입력해주세요.', - duplicatedTeamNameError: '중복된 팀 이름입니다.', - duplicatedTeamNameFailure: '이미 존재하는 팀 이름이라 생성에 실패했어요.', - invalidRequestFailure: '요청 값이 올바르지 않아 팀 생성에 실패했어요.', - unauthorizedFailure: '팀을 생성할 권한이 없어 실패했어요.', - defaultFailure: '팀 생성에 실패했어요. 잠시 후 다시 시도해주세요.', -} as const; diff --git a/src/app/addteam/_domain/hooks/useCreateTeam.ts b/src/app/addteam/_domain/hooks/useCreateTeam.ts deleted file mode 100644 index 0f984a1..0000000 --- a/src/app/addteam/_domain/hooks/useCreateTeam.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Group } from '@/shared/apis/groups/types'; -import { useCreateGroupMutation } from '@/shared/queries/groups/useCreateGroupMutation'; -import { groupsKeys } from '@/shared/queries/groups/queryKeys'; -import { QueryClient, useQueryClient } from '@tanstack/react-query'; -import { CREATE_TEAM_MESSAGES } from '../constants/createTeam'; -import { isDuplicated, normalizeTeamName } from '../utils/duplicationCalculator'; - -function getCachedTeamNames(queryClient: QueryClient) { - const cachedDetails = queryClient.getQueriesData({ - queryKey: groupsKeys.details(), - }); - - return cachedDetails - .map(([, group]) => group?.name) - .filter((name): name is string => typeof name === 'string'); -} - -export function useCreateTeam() { - const queryClient = useQueryClient(); - const createGroupMutation = useCreateGroupMutation(); - - const createTeam = async (name: string) => { - const normalizedName = normalizeTeamName(name); - if (!normalizedName) { - throw new Error(CREATE_TEAM_MESSAGES.emptyTeamNameError); - } - - const cachedNames = getCachedTeamNames(queryClient); - if (isDuplicated(cachedNames, normalizedName)) { - throw new Error(CREATE_TEAM_MESSAGES.duplicatedTeamNameError); - } - - return createGroupMutation.mutateAsync({ name: normalizedName }); - }; - - return { - ...createGroupMutation, - createTeam, - }; -} diff --git a/src/app/addteam/_domain/hooks/useJoinTeam.ts b/src/app/addteam/_domain/hooks/useJoinTeam.ts deleted file mode 100644 index f0fcb3e..0000000 --- a/src/app/addteam/_domain/hooks/useJoinTeam.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useAcceptInvitationMutation } from '@/shared/queries/groups/useAcceptInvitationMutation'; -import { useCurrentUserQuery } from '@/shared/queries/user/useCurrentUserQuery'; - -// 팀 링크(URL) 또는 토큰 문자열에서 inviteToken을 추출 -function extractToken(teamLink: string): string { - try { - const url = new URL(teamLink.trim()); - const token = url.searchParams.get('token'); - if (token) return token; - } catch { - // URL 파싱 실패 시 입력값 자체를 토큰으로 사용 - } - return teamLink.trim(); -} - -export function useJoinTeam() { - const acceptInvitationMutation = useAcceptInvitationMutation(); - const { data: currentUser } = useCurrentUserQuery(); - - const joinTeam = async (teamLink: string) => { - const token = extractToken(teamLink); - if (!token) { - throw new Error('팀 링크를 입력해주세요.'); - } - const userEmail = currentUser?.email; - if (!userEmail) { - throw new Error('로그인이 필요합니다.'); - } - return acceptInvitationMutation.mutateAsync({ userEmail, token }); - }; - - return { - ...acceptInvitationMutation, - joinTeam, - }; -} diff --git a/src/app/addteam/_domain/interfaces/feedback.ts b/src/app/addteam/_domain/interfaces/feedback.ts deleted file mode 100644 index fbc5d6d..0000000 --- a/src/app/addteam/_domain/interfaces/feedback.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface CreateTeamFeedback { - type: 'success' | 'error'; - message: string; -} diff --git a/src/app/addteam/_domain/queries/useUploadImageMutation.ts b/src/app/addteam/_domain/queries/useUploadImageMutation.ts deleted file mode 100644 index a50b6ca..0000000 --- a/src/app/addteam/_domain/queries/useUploadImageMutation.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { useMutation } from '@tanstack/react-query'; -import { uploadImage } from '../apis/image'; - -export function useUploadImageMutation() { - return useMutation({ - mutationFn: uploadImage, - }); -} diff --git a/src/app/addteam/_domain/styles/common.module.css b/src/app/addteam/_domain/styles/common.module.css deleted file mode 100644 index e9f86bb..0000000 --- a/src/app/addteam/_domain/styles/common.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.flexCol { - display: flex; - flex-direction: column; -} - -.flexColCenter { - display: flex; - flex-direction: column; - align-items: center; -} - -.flexCenter { - display: flex; - align-items: center; - justify-content: center; -} diff --git a/src/app/addteam/_domain/svg/noTeamImg.svg b/src/app/addteam/_domain/svg/noTeamImg.svg deleted file mode 100644 index 6ade2c6..0000000 --- a/src/app/addteam/_domain/svg/noTeamImg.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/app/addteam/_domain/utils/duplicationCalculator.ts b/src/app/addteam/_domain/utils/duplicationCalculator.ts deleted file mode 100644 index 4a83666..0000000 --- a/src/app/addteam/_domain/utils/duplicationCalculator.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function normalizeTeamName(name: string) { - return name.trim(); -} - -function normalizeForCompare(name: string) { - return normalizeTeamName(name).toLowerCase(); -} - -export function isDuplicated(existingNames: string[], inputName: string) { - const normalizedInput = normalizeForCompare(inputName); - return existingNames.some((name) => normalizeForCompare(name) === normalizedInput); -} diff --git a/src/app/addteam/_domain/utils/getCreateTeamFailureMessage.ts b/src/app/addteam/_domain/utils/getCreateTeamFailureMessage.ts deleted file mode 100644 index 3a39843..0000000 --- a/src/app/addteam/_domain/utils/getCreateTeamFailureMessage.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { CREATE_TEAM_MESSAGES } from '../constants/createTeam'; - -const DUPLICATED_STATUS = 'status: 409'; -const INVALID_REQUEST_STATUS = 'status: 400'; -const UNAUTHORIZED_STATUSES = ['status: 401', 'status: 403'] as const; - -function hasStatusMessage(message: string, status: string) { - return message.includes(status); -} - -export function getCreateTeamFailureMessage(error: unknown) { - if (!(error instanceof Error)) { - return CREATE_TEAM_MESSAGES.defaultFailure; - } - - const { message } = error; - - if (message === CREATE_TEAM_MESSAGES.emptyTeamNameError) { - return CREATE_TEAM_MESSAGES.emptyTeamNameError; - } - - if ( - message === CREATE_TEAM_MESSAGES.duplicatedTeamNameError || - hasStatusMessage(message, DUPLICATED_STATUS) - ) { - return CREATE_TEAM_MESSAGES.duplicatedTeamNameFailure; - } - - if (hasStatusMessage(message, INVALID_REQUEST_STATUS)) { - return CREATE_TEAM_MESSAGES.invalidRequestFailure; - } - - if (UNAUTHORIZED_STATUSES.some((status) => hasStatusMessage(message, status))) { - return CREATE_TEAM_MESSAGES.unauthorizedFailure; - } - - return CREATE_TEAM_MESSAGES.defaultFailure; -} diff --git a/src/app/addteam/create/page.tsx b/src/app/addteam/create/page.tsx deleted file mode 100644 index ccecf8b..0000000 --- a/src/app/addteam/create/page.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; -import CreateTeamCard from '../_domain/components/CreateTeamCard'; -import { CREATE_TEAM_MESSAGES } from '../_domain/constants/createTeam'; -import { useCreateTeam } from '../_domain/hooks/useCreateTeam'; -import { getCreateTeamFailureMessage } from '../_domain/utils/getCreateTeamFailureMessage'; -import type { CreateTeamFeedback } from '../_domain/interfaces/feedback'; - -export default function CreateTeamPage() { - const [teamName, setTeamName] = useState(''); - const [createTeamFeedback, setCreateTeamFeedback] = useState(null); - const { createTeam, isPending } = useCreateTeam(); - const router = useRouter(); - - const isSubmitDisabled = !teamName.trim() || isPending; - - const handleSubmit = async () => { - if (isSubmitDisabled) return; - - try { - const group = await createTeam(teamName); - setTeamName(''); - setCreateTeamFeedback({ type: 'success', message: CREATE_TEAM_MESSAGES.success }); - router.push(`/${group.id}`); - } catch (error) { - setCreateTeamFeedback({ type: 'error', message: getCreateTeamFailureMessage(error) }); - } - }; - - const handleTeamNameChange = (value: string) => { - setTeamName(value); - setCreateTeamFeedback(null); - }; - - return ( - - ); -} diff --git a/src/app/addteam/join/page.tsx b/src/app/addteam/join/page.tsx deleted file mode 100644 index 1ecac56..0000000 --- a/src/app/addteam/join/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client'; - -import { Suspense, useState } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; -import JoinTeamCard from '../_domain/components/JoinTeamCard'; -import { useJoinTeam } from '../_domain/hooks/useJoinTeam'; -import type { CreateTeamFeedback } from '../_domain/interfaces/feedback'; - -function JoinTeamPageContent() { - const searchParams = useSearchParams(); - // 초대 링크로 직접 접근 시 URL의 현재 주소를 입력값으로 자동 설정 - const [teamLink, setTeamLink] = useState(() => { - if (typeof window === 'undefined') return ''; - return searchParams.get('token') ? window.location.href : ''; - }); - const [feedback, setFeedback] = useState(null); - const { joinTeam, isPending } = useJoinTeam(); - const router = useRouter(); - - const isSubmitDisabled = !teamLink.trim() || isPending; - - const handleSubmit = async () => { - if (isSubmitDisabled) return; - - try { - const group = await joinTeam(teamLink); - setFeedback({ type: 'success', message: '팀에 참여했습니다.' }); - router.push(`/${group.id}`); - } catch { - setFeedback({ type: 'error', message: '유효하지 않은 팀 링크입니다.' }); - } - }; - - const handleTeamLinkChange = (value: string) => { - setTeamLink(value); - setFeedback(null); - }; - - return ( - - ); -} - -export default function JoinTeamPage() { - return ( - - - - ); -} diff --git a/src/app/addteam/layout.tsx b/src/app/addteam/layout.tsx deleted file mode 100644 index 5167758..0000000 --- a/src/app/addteam/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { MobileHeader, Sidebar } from '@/components/sidebar'; -import styles from './page.module.css'; - -export default function AddTeamLayout({ children }: { children: React.ReactNode }) { - return ( -
- -
- -
-
{children}
-
- ); -} diff --git a/src/app/addteam/page.module.css b/src/app/addteam/page.module.css deleted file mode 100644 index dab7364..0000000 --- a/src/app/addteam/page.module.css +++ /dev/null @@ -1,41 +0,0 @@ -.page { - --layout-padding-desktop: 24px; - --layout-padding-mobile: 16px; - --create-card-width-desktop: min(90vw, 480px); - --create-card-width-mobile: min(100%, 343px); - --join-card-width-desktop: min(90vw, 550px); - --join-input-width-desktop: min(100%, 460px); - --control-height-desktop: 48px; - --control-height-mobile: 44px; - - display: flex; - min-height: 100vh; - background: var(--color-background-secondary); -} - -.mobileOnlyHeader { - display: none; -} - -.mainContents { - display: flex; - align-items: center; - justify-content: center; - flex: 1; - padding: var(--layout-padding-desktop); -} - -@media (max-width: 767px) { - .page { - flex-direction: column; - } - - .mobileOnlyHeader { - display: block; - } - - .mainContents { - width: 100%; - padding: var(--layout-padding-mobile); - } -} diff --git a/src/app/addteam/page.tsx b/src/app/addteam/page.tsx deleted file mode 100644 index 9ca589a..0000000 --- a/src/app/addteam/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import NoTeamState from './_domain/components/NoTeamState'; - -export default function AddTeamPage() { - return ; -}