-
Notifications
You must be signed in to change notification settings - Fork 5
[feat, refactor] #190 팀 참여 페이지 작성 / 팀 생성, 수정 페이지 리팩토링 #200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
224432f
f76085f
a316756
b100e78
8d87c5c
28884e1
d550d38
5075396
a22dea3
b425841
065005f
c712054
81d7a10
311eed0
7fa5167
272c087
559e6b5
790a936
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import InputBase from '@/components/common/Input/InputBase'; | ||
| import Button from '@/components/common/Button'; | ||
|
|
||
| export interface JoinTeamFormProps { | ||
| link: string; | ||
| onLinkChange: (v: string) => void; | ||
| onSubmit: () => void; | ||
| error?: string; | ||
| isLoading?: boolean; | ||
| } | ||
|
|
||
| export default function JoinTeamForm({ | ||
| link, | ||
| onLinkChange, | ||
| onSubmit, | ||
| error, | ||
| isLoading, | ||
| }: JoinTeamFormProps) { | ||
| const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { | ||
| if (e.key === 'Enter') { | ||
| e.preventDefault(); | ||
| onSubmit(); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="text-md-regular tablet:w-[460px] tablet:text-lg-regular flex w-[343px] flex-col items-center"> | ||
| <h1 className="text-2xl-medium laptop:text-4xl-medium tablet:mb-20 mb-6"> | ||
| 팀 참여하기 | ||
| </h1> | ||
|
|
||
| <div className="mb-10 w-full self-start"> | ||
| <InputBase | ||
| id="teamLink" | ||
| title="팀 링크" | ||
| placeholder="팀 링크를 입력해주세요." | ||
| value={link} | ||
| isInvalid={Boolean(error)} | ||
| onChange={(e) => onLinkChange(e.target.value)} | ||
| onKeyDown={handleKeyDown} | ||
| autoComplete="off" | ||
| titleClassName="mb-3" | ||
| containerClassName="h-11 tablet:h-12 bg-slate-800" | ||
| inputClassName="w-full h-11 tablet:h-12" | ||
| /> | ||
| {error && <p className="text-md-medium mt-2 text-red-500">{error}</p>} | ||
| </div> | ||
|
|
||
| <div className="mb-6 w-full"> | ||
| <Button | ||
| variant="primary" | ||
| styleType="filled" | ||
| size="lg" | ||
| radius="sm" | ||
| className="text-lg-semibold w-full" | ||
| onClick={onSubmit} | ||
| disabled={!link.trim() || isLoading} | ||
| > | ||
| {isLoading ? '처리 중...' : '참여하기'} | ||
| </Button> | ||
| </div> | ||
|
|
||
| <p className="text-md-regular tablet:text-lg-regular"> | ||
| 공유받은 팀 링크를 입력해 참여할 수 있어요. | ||
| </p> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,66 @@ | ||
| 'use client'; | ||
|
|
||
| import { useState, useEffect } from 'react'; | ||
| import { useRouter } from 'next/navigation'; | ||
| import { ROUTES } from '@/constants/routes'; | ||
| import JoinTeamForm from '@/app/(team)/_components/JoinTeamForm'; | ||
| import { postGroupInvitation } from '@/lib/apis/group'; | ||
| import { useMemberships } from '@/hooks/useMemberships'; | ||
| import { INVITATION_ERROR_MAP } from '@/utils/errorMap'; | ||
|
|
||
| const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/; | ||
|
|
||
| export default function JoinTeamPage() { | ||
| return <div>JoinTeamPage</div>; | ||
| const router = useRouter(); | ||
| const { memberships } = useMemberships(true); | ||
|
|
||
| const [link, setLink] = useState(''); | ||
| const [error, setError] = useState<string>(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| const [isLoading, setLoading] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| setError(undefined); | ||
| }, [link]); | ||
|
|
||
|
Comment on lines
+21
to
+24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 유저가 링크를 수정할 경우, 이전 에러 메시지가 남아있지 않도록 기존의 에러 메시지를 삭제하는 패턴이 맞을까요? 해당 방식은 만약 사용자가 이전과 동일한, 잘못된 값을 다시 입력하더라도 에러 메시지가 사라졌다가 다시 나타나는, 불필요한 상태 업데이트가 발생할 수 있습니다. 입력값이 바뀌어도 기존 에러 메시지와 동일한 문제가 반복된다면,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사용자가 입력칸에 변화를 주면 이전의 에러메세지는 사라지는 것이 자연스럽다고 생각했습니다. |
||
| const handleJoin = async () => { | ||
| if (!jwtRegex.test(link.trim())) { | ||
| setError('유효한 링크가 아닙니다.'); | ||
| return; | ||
| } | ||
| const email = memberships[0]?.userEmail; | ||
| if (!email) { | ||
| setError('사용자 정보를 불러올 수 없습니다.'); | ||
| return; | ||
| } | ||
|
|
||
| setLoading(true); | ||
| try { | ||
| const res = await postGroupInvitation({ | ||
| body: { token: link.trim(), userEmail: email }, | ||
| }); | ||
| if (res?.groupId == null) { | ||
| throw new Error(res?.message || '초대 수락 실패'); | ||
| } | ||
| router.push(ROUTES.TEAM(res.groupId)); | ||
| } catch (err: unknown) { | ||
| const raw = err instanceof Error ? err.message : String(err); | ||
| setError( | ||
| INVITATION_ERROR_MAP[raw] ?? '초대 수락 중 오류가 발생했습니다.' | ||
| ); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="flex w-full justify-center pt-35"> | ||
| <JoinTeamForm | ||
| link={link} | ||
| onLinkChange={setLink} | ||
| onSubmit={handleJoin} | ||
| error={error} | ||
| isLoading={isLoading} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,15 +40,19 @@ export function useUpdateGroup(teamId: number) { | |
| const qc = useQueryClient(); | ||
| return useMutation({ | ||
| mutationFn: async ( | ||
| data: GroupBody & { file?: File } | ||
| data: GroupBody & { file?: File; removeImage?: boolean } | ||
| ): Promise<GroupResponse | null> => { | ||
| let imageUrl: string | undefined; | ||
| const body: { name: string; image?: string | null } = { name: data.name }; | ||
|
|
||
| if (data.file) { | ||
| imageUrl = await uploadImage(data.file); | ||
| const url = await uploadImage(data.file); | ||
| body.image = url; | ||
| } else if (data.removeImage) { | ||
| body.image = null; | ||
| } | ||
| return patchGroupById({ | ||
| groupId: teamId, | ||
| body: { name: data.name, ...(imageUrl ? { image: imageUrl } : {}) }, | ||
| body, | ||
|
Comment on lines
+48
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| }); | ||
| }, | ||
| onSuccess: () => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export const INVITATION_ERROR_MAP: Record<string, string> = { | ||
| '이미 그룹에 소속된 유저입니다.': '이미 참여 중인 팀입니다', | ||
| '유효하지 않은 초대입니다.': '유효한 링크가 아닙니다.', | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useEffect로props로 전달되는initialName이 변경되면, 내부 file 상태도 초기화시킨 거군요~!이전에 업로드한 파일 정보도 함께 초기화시킬 수 있어서 좋은 방법이네요 👍