Skip to content

Commit

Permalink
Merge pull request #242 from Nexters/feature/cast-member-order
Browse files Browse the repository at this point in the history
feat: 출연진 팀 내 팀원 순서 변경 기능 구현
  • Loading branch information
Puterism authored Nov 16, 2024
2 parents c0b9f48 + 3e7ea3b commit a7d1f28
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 165 deletions.
10 changes: 6 additions & 4 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 1 addition & 5 deletions apps/admin/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import './index.css';

import { QueryClientProvider } from '@boolti/api';
import { BooltiUIProvider } from '@boolti/ui';
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { setDefaultOptions } from 'date-fns';
import { ko } from 'date-fns/locale';
import {
Expand Down Expand Up @@ -157,9 +155,7 @@ const routes: RouteObject[] = [
<QueryClientProvider>
<BooltiUIProvider>
<LazyMotion features={domAnimation}>
<DndProvider backend={HTML5Backend}>
<Outlet />
</DndProvider>
<Outlet />
</LazyMotion>
</BooltiUIProvider>
</QueryClientProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,20 @@ const InputWrapper = styled.div<InputWrapperProps>`
&:focus-within {
border-color: ${({ theme, isError }) =>
isError ? theme.palette.status.error : theme.palette.grey.g70};
isError ? theme.palette.status.error : theme.palette.grey.g70};
}
`;

const Handle = styled.div`
display: inline-flex;
align-items: center;
justify-content: center;
color: ${({ theme }) => theme.palette.grey.g40};
margin-top: 12px;
margin-right: 8px;
cursor: move;
`

const TextFieldWrap = styled.div`
margin-bottom: 28px;
Expand Down Expand Up @@ -91,6 +101,8 @@ const Row = styled.div`
justify-content: center;
align-items: flex-start;
margin-bottom: 20px;
background-color: ${({ theme }) => theme.palette.grey.w};
border-radius: 4px;
`;

const TrashCanButton = styled.button`
Expand Down Expand Up @@ -174,6 +186,7 @@ const ErrorMessage = styled.span`
export default {
ShowInfoFormLabel,
InputWrapper,
Handle,
HashTag,
Input,
Row,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { useDrag, useDrop } from "react-dnd";
import { ClearIcon, MenuIcon, TrashIcon, UserIcon } from '@boolti/icon';
import { Member } from '@boolti/api';
import { replaceUserCode } from '~/utils/replace';
import Styled from './ShowCastInfoFormDialogContent.styles';
import { TempShowCastInfoFormInput } from ".";
import { Control, Controller } from "react-hook-form";
import { useRef } from "react";

interface DragItem {
id: number
index: number
}

interface ShowCastInfoMemberRowProps {
control: Control<TempShowCastInfoFormInput>;
field: Partial<Member> & { id: number };
index: number;
isFieldBlurred: { userCode: boolean; roleName: boolean };
onSetUser: (userCode: string) => void;
onResetUser: () => void;
onBlurRoleName: () => void;
onDelete: () => void
onDropHover: (draggedItemId: number, hoverIndex: number) => void;
onDrop?: () => void;
}

const ShowCastInfoMemberRow = ({ control, field, index, isFieldBlurred, onSetUser, onResetUser, onBlurRoleName, onDelete, onDropHover, onDrop }: ShowCastInfoMemberRowProps) => {
const ref = useRef<HTMLDivElement>(null)
const [{ isDragging }, drag, preview] = useDrag<DragItem, unknown, { isDragging: boolean }>(() => ({
type: 'castMember',
previewOptions: {
captureDraggingState: true,
},
item: { id: field.id, index },
collect: (monitor) => ({
isDragging: monitor.isDragging()
}),
}))
const [, drop] = useDrop<DragItem>({
accept: 'castMember',
hover(item: DragItem, monitor) {
if (!ref.current) return;
if (!monitor.canDrop()) return;
if (item.id === field.id) return;

const dragIndex = item.index;
const hoverIndex = index;

const hoverBoundingRect = ref.current.getBoundingClientRect();
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
if (!clientOffset) return;

const hoverClientY = clientOffset.y - hoverBoundingRect.top;
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return;
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return;

item.index = hoverIndex;

onDropHover(item.id, index);
},
drop() {
onDrop?.()
}
})

preview(drop(ref))

return (
<Styled.Row
ref={ref}
style={{
opacity: isDragging ? 0.4 : 1,
}}
>
<Styled.Handle ref={drag}>
<MenuIcon />
</Styled.Handle>
<Controller
control={control}
render={({ field: { onChange, onBlur } }) => {
const value = field.userCode;
const isError = Boolean(
isFieldBlurred.userCode ? !value || !field.userNickname : false,
);
return (
<Styled.FieldWrap>
<Styled.InputWrapper isError={isError}>
{field.userNickname ? (
<>
{field.userImgPath ? (
<Styled.UserImage
style={
{
'--imgPath': `url(${field.userImgPath})`,
} as React.CSSProperties
}
/>
) : (
<UserIcon size={32} />
)}
<Styled.Username>{field.userNickname}</Styled.Username>
<Styled.RemoveButton
onClick={() => {
onChange(undefined);
onResetUser();
}}
>
<ClearIcon />
</Styled.RemoveButton>
</>
) : (
<>
<Styled.HashTag>#</Styled.HashTag>
<Styled.Input
placeholder="식별 코드"
required
onChange={(e) => {
const nextValue = replaceUserCode(e.target.value);
onChange(nextValue);
}}
onBlur={async (event) => {
onBlur();
onSetUser(event.target.value);
}}
value={value ?? ''}
/>
</>
)}
</Styled.InputWrapper>
{isError && <Styled.ErrorMessage>필수 입력사항입니다.</Styled.ErrorMessage>}
</Styled.FieldWrap>
);
}}
name={`members.${index}.userCode`}
/>
<Controller
control={control}
rules={{
required: true,
}}
render={({ field: { onChange, onBlur } }) => {
const value = field.roleName;
const isError = isFieldBlurred.roleName && !value;
return (
<Styled.FieldWrap>
<Styled.InputWrapper isError={isFieldBlurred.roleName && !value}>
<Styled.Input
placeholder="역할"
required
onChange={onChange}
onBlur={() => {
onBlur();
onBlurRoleName();
}}
value={value ?? ''}
/>
</Styled.InputWrapper>
{isError && <Styled.ErrorMessage>필수 입력사항입니다.</Styled.ErrorMessage>}
</Styled.FieldWrap>
);
}}
name={`members.${index}.roleName`}
/>
<Styled.TrashCanButton
onClick={onDelete}
>
<TrashIcon />
</Styled.TrashCanButton>
</Styled.Row>
);
}

export default ShowCastInfoMemberRow
Loading

0 comments on commit a7d1f28

Please sign in to comment.